<#920 Security posture adjusments for risk calcula...
# osctrl
g
#920 Security posture adjusments for risk calculation Pull request opened by javuto Summary Redesigns the posture risk scoring in
pkg/posture/scoring.go
to be realistic and defensible from an ISO 27001 / SOC 2 perspective. The trigger was a false positive: on Debian/Ubuntu servers, an empty
rpm_packages
result counted as a permanent warning — but a Debian host should not have RPM packages. Reworking that surfaced several other false positives that made whole platforms look unhealthy. What was wrong • Inapplicable checks counted as findings. Linux profiles collect both
packages_deb
and `packages_rpm`; whichever one doesn't apply to the distro is always empty, so every Linux node carried at least one bogus warning. Same for macOS nodes without Homebrew. • Every healthy BitLocker machine failed disk encryption. Windows profiles store BitLocker rows under the
disk_encryption
category, but the rule only understood the Linux/macOS
encrypted
field — rows with
protection_status
were counted as unencrypted, so fully protected Windows nodes scored a critical fail. • Every normal Windows server failed the ports control. RDP (3389) and SMB (445) were hard fails, yet they listen on effectively every Windows server. • Score depended on how many checks ran. Points were additive, so a well-monitored node accumulated more risk points than a barely-monitored one, and warnings (at 50% weight) could outrank real failures. The new model 1. Applicability-aware controls. A control can draw evidence from multiple posture categories. All five package sources (deb, rpm, Windows programs, macOS apps, Homebrew) feed a single "Software inventory" control (ISO A.5.9) that passes if any source has data and warns only when every collected source is empty (i.e. osquery genuinely can't read the tables). Controls whose categories were never collected are skipped entirely — "not applicable" is no longer reported as "warning". 2. Normalized score.
total_score = 100 × earned risk / max possible risk of the controls actually evaluated
. Scores are now comparable across platforms with different check counts, and a node is not penalized for being monitored more thoroughly. Warnings earn 25% of a control's weight (was 50%) — they are review items, not confirmed exposures. 3. Exception-driven risk level. Auditors reason in nonconformities, not weighted averages: any failing critical control (e.g. unencrypted disk) makes the node
critical
outright; a failing high control raises it to at least `high`; otherwise the score thresholds decide. 4. Realistic rule semantics. • Disk encryption handles both schemas per row (
encrypted
and BitLocker's
protection_status
, including osquery's numeric
1
). Unprotected OS drive (C:) → fail; nothing encrypted → fail; partial encryption (unencrypted `/boot`/swap next to a LUKS root) → warn instead of critical fail. • Listening ports: plaintext-auth services (telnet, ftp) fail; RDP/SMB/SNMP/VNC warn with a "verify exposure" detail. • macOS sharing: removed dead code; any enabled sharing service now warns with the list (previously up to two enabled services silently passed). 5. Corrected ISO 27001:2022 control mapping — e.g. disk encryption → A.8.24 (Use of cryptography; A.8.5 is actually Secure authentication), software inventory → A.5.9, listening ports → A.8.20, SSH keys → A.5.17, browser extensions → A.8.19. Compatibility • The
PostureScore
JSON contract is unchanged (
total_score
,
risk_level
,
controls[]
with `pass`/`warn`/`fail`, pass/warn/fail counts) — no SPA changes required. • Risk levels remain `low`/`medium`/`high`/`critical` with the same thresholds, so node health projection is untouched. • Expect risk levels to shift on existing fleets: false warnings disappear (scores drop for Debian/RHEL/macOS nodes), while nodes with a failing critical control (unencrypted disk) now report
critical
instead of whatever the additive sum happened to reach. Test plan • 16 new tests in `pkg/posture/scoring_test.go`: empty-RPM-on-Debian / empty-deb-on-RHEL / no-Homebrew-on-macOS produce no findings; BitLocker rows under
disk_encryption
pass; unprotected C: fails and escalates to critical; partial Linux encryption warns; telnet fails while RDP/SMB warn; healthy node scores 0/low; normalization math; skipped uncollected controls; empty input. • Updated
TestProjectNodeAddsPostureRiskWhenPostureEnabled
expectation from
high
to
critical
(its fixture includes an unencrypted disk, which now correctly escalates). •
go test ./pkg/posture/... ./cmd/api/handlers/...
passes;
go vet
and
gofmt
clean. jmpsec/osctrl