Skip to content

Audit fixes: 15 MEDIUM findings (vuln engine, parsers, discovery, auth, analysis) - #27

Open
Krishcalin wants to merge 5 commits into
mainfrom
audit-mediums
Open

Krishcalin wants to merge 5 commits into
mainfrom
audit-mediums

Conversation

@Krishcalin

Copy link
Copy Markdown
Owner

Resolves the 15 MEDIUM audit findings that live on main, in five reviewable commits, each fix paired with the regression test the audit noted was missing.

Three of the 18 mediums are not here — they live in this session's own new code on unmerged branches and will be fixed there:

Commits

Vuln engine (24dc769)

  • vuln_view ranked/displayed the headline CVE on CVSS v3.1 alone, zeroing v4-only CVEs → rank across all scored versions, display the highest.
  • summary().devices_unassessed mixed an unscoped/org-unfiltered/archived-inclusive denominator with scoped totals → both sides now describe one population (this org, visible groups, non-archived).
  • matcher read fix releases only from advisory.fixed (CSAF); NVD states them as the range's versionEndExcluding → harvest the range fix bound too (last_affected deliberately not read).

Parsers (a662f1f)

  • arista ip route <prefix> <intf> <gw> dropped the gateway → capture the optional trailing gateway.
  • aws cross-account sg-... refs left untyped (MISSING) → backfilled as security-group (externally resolved).
  • azure regional service tags (Storage.EastUS) misclassified as literals by a dot test → real ip_network parse.
  • nsx group/service catalogue keyed on display_name while rules reference the path id → keyed by id.

Discovery (06f42e6)

  • TCP connection refused recorded like a timeout → host_alive flag marks the host found without opening the port.
  • mid-run ICMP-degradation notes dropped for non-responding hosts → notes collected before the liveness short-circuit.

Auth / config / migration (37b9d0d)

  • TOTP replay: guard recorded the current step, not the code's own step → matched_totp_step stores the matched step; a used code is refused for the rest of its window.
  • production config didn't require SECRET_KEY → fails at boot when it comes only from the random default_factory.
  • migration 0017 omitted ix_oidc_login_states_org_id → added to the head migration; alembic check now reports no drift.

Analysis (a1dd434)

  • topology range arrival short-circuited before the subdividing guard → guard moved ahead of arrival/routing; a part-connected/part-routed range returns UNKNOWN.
  • trend report compared across mismatched scopes → reports stamp a scope_fingerprint; a trend refuses when the two sides describe different populations.
  • AAA partial-parse flagged devices unregistered/HIGH on the strength of other servers → a server with an unreadable client list withholds the verdict and states the gap.

Testing

Each group's suite runs green locally (PowerShell venv). alembic check confirms no migration drift. Adjacent suites (auth flow, vuln wiring, report delivery) also pass; 4 report-delivery skips are an unrelated environmental pycryptodomex issue.

🤖 Generated with Claude Code

Krishcalin and others added 5 commits September 30, 2026 08:29
…aths

Three medium findings in the vulnerability views, all "confident number over the
wrong population" defects:

- vuln_view._enrich ranked and displayed the headline CVE on CVSS v3.1 alone. A
  CVE scored only under CVSS 4.0 (increasingly common post-2024; both NVD and CSAF
  ingest it) has a NULL cvss31, so it sorted to the bottom at 0.0 and its row cell
  was blank. Rank across every scored version (_score) and display whichever version
  scores highest (_headline_cvss).

- vuln_view.summary() derived devices_unassessed from an unscoped, org-unfiltered,
  archived-inclusive device count while total/by_severity were scoped and
  org-filtered. A group-scoped analyst was told hundreds of devices they cannot see
  are unassessed. Numerator and denominator now describe one population: this org,
  this principal's visible groups, non-archived (matching assess_all).

- matcher._fixed_versions read only advisory.fixed (the CSAF form). NVD never
  populates it and states the fix only as the affected range's exclusive upper bound
  (versionEndExcluding, constraint.fixed), so the upgrade-path view was blind to every
  NVD vulnerability. Harvest the range fix bound too; last_affected (still-vulnerable)
  is deliberately not read.

Regression tests: a v4-only CVE still shows its score; an archived device does not
inflate the unassessed count; an NVD-shape range fix feeds the upgrade view while an
inclusive last_affected bound is never offered as a fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s, NSX id key

Four parser findings, each one dropping or misclassifying a rule member so the
firewall/path analysis reads a wrong answer as complete:

- arista/eos: `ip route <prefix> <intf> <gw>` captured only the first token, dropping
  the trailing gateway. The route stored as interface-only, so the path walk treated it
  as directly-attached and lost the forwarding hop. Regex now captures an optional
  trailing gateway address (a bare administrative distance or name/tag keyword does not
  capture); an interface-only route still has no next hop.

- cloud/aws: a rule referencing a peered/cross-account group (sg-... absent from the
  export) got no NetworkObject and fell to the unresolved/MISSING bucket, advising a
  re-collection that can never produce it. Backfilled as type security-group
  (externally resolved), alongside the existing pl- prefix-list handling.

- cloud/azure: the service-tag guard `any(ch in member for ch in '.:/')` treated the dot
  in a regional tag (Storage.EastUS, Sql.WestEurope) as proof of an address literal, so
  it was never typed service-tag and resolved to nothing. Replaced with an actual
  ip_network parse; only a value that parses as an address/network is a literal.

- vmware/nsx: the group (and service) catalogue keyed on display_name while rules refer
  to groups by path id (reduced via _leaf). Any API/Terraform object whose display_name
  differs from its id (id=grp-4821, display_name='Web Servers') was catalogued under a
  name no rule uses, so every referencing rule lost its members. Keyed by id now, per
  the module's own docstring.

Regression tests cover each: interface+gateway route, cross-account SG typing, regional
tag vs real CIDR, and id-keyed group resolution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ive silence

Two discovery findings that made a partial-coverage run read as complete:

- transport.send_tcp_connect caught ConnectionRefusedError under the generic OSError
  and returned responded=False, indistinguishable from a timeout. A refusal is an RST
  from the host itself — the port is closed but the host is provably up. A hardened
  device that drops ICMP and closes every scanned port was therefore reported absent.
  ProbeOutcome gains a host_alive flag (distinct from responded, so a refused port is
  not mistaken for an open one); HostProber marks the host responsive on a refusal
  without opening the port or attempting a follow-up read.

- executor._record_batch collected per-host notes only after the `if not responded:
  continue` short-circuit. The ICMP-degradation note rides on whichever host was being
  probed when echo stopped, and that host usually does not respond — so the very caveat
  that makes the run partial was the one dropped, and "found N hosts" read as complete.
  Notes are now collected before the liveness short-circuit.

Regression tests: an injected refusal proves liveness at both the probe and prober
level without opening the port; a caveat on a non-responding host still reaches the run
row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… org_id index

- services/auth.complete_mfa recorded last_used_step as the CURRENT time-step, not the
  step the accepted code belongs to. A code for step S stays valid through S+window, so
  the current-step guard left it replayable: an intercepted code yielded a second
  session one step later. Added core.security.matched_totp_step, which returns the step
  the code actually matched; the guard now stores and compares that, so a used code is
  refused for the rest of its window while a genuinely newer code still advances.

- core/config: the production guard checked debug, cookie_secure and CORS but never that
  SECRET_KEY was explicitly set. Unset, it fell to a default_factory random per-process
  key — tokens signed by one worker failing on another, every restart invalidating every
  session, all silently. Production now fails at boot when secret_key is not in
  model_fields_set (i.e. came only from the factory).

- migration 0017: oidc_login_states inherits OrgMixin (org_id index=True) but the
  migration never created ix_oidc_login_states_org_id, so `alembic check` saw drift (C-5)
  and the tenancy column was unindexed. Added to the head migration directly (it is the
  head, unlike 0009 which needed corrective 0011), avoiding a multi-head collision.
  Verified: `alembic check` now reports no new operations.

Regression tests: matched step is the code's own step not the current one and advances
for a fresh code; a TOTP code cannot be completed twice; an unset SECRET_KEY is rejected
in production while an explicit one is accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…AA parse

Three findings where an analysis reported a confident answer over the wrong basis:

- topology/path: the range-destination arrival check `serves(dst)` tested only the
  representative (low) address, short-circuiting before the routes_subdividing guard. A
  range whose low half is directly connected and high half is routed away was reported
  fully arrived/ROUTED. Moved the subdividing guard ahead of the arrival and routing
  branches, so a range any prefix cuts across returns UNKNOWN with the prefixes named.

- services/reporting._trend: `get()` enforces only org_id, so a group-scoped caller
  could fetch a whole-estate report and subtract its frozen counts from a scoped current
  summary, fabricating deltas ("960 findings closed"). Reports now stamp a
  scope_fingerprint (unrestricted vs the sorted visible group ids) at generation; a trend
  refuses when the earlier report's fingerprint differs from the caller's, including
  pre-fingerprint reports that cannot be shown comparable.

- services/aaa_correlation: `product not in _SERVER_PRODUCTS or not config.clients`
  conflated "not an AAA server" with "an AAA server whose client list was empty/unparsed"
  and silently dropped both. A device registered only on the unparsed server was then
  flagged unregistered/HIGH on the strength of the other servers. Split the conditions:
  a known server with no readable clients is recorded, registration_reliable goes false,
  the unregistered verdict is withheld, and a limitation names the servers to re-collect.

Regression tests: a part-connected/part-routed range returns UNKNOWN; a trend across
mismatched scopes is refused while same-scope still compares; an unreadable client list
withholds the unregistered verdict and states the gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Krishcalin added a commit that referenced this pull request Sep 30, 2026
Both dr_sets and dr_set_members inherit OrgMixin (org_id index=True, DATA-04), so the ORM
declares ix_dr_sets_org_id and ix_dr_set_members_org_id, but migration 0018 created neither
— an autogenerate diff that fails the C-5 alembic-check gate, and an unindexed tenancy
column on tables every scoped query filters. Created both in the migration, mirroring every
other OrgMixin table in the chain.

Regression test asserts both indexes exist in pg_indexes after migration.

(The sibling ix_oidc_login_states_org_id omission on migration 0017 is the same finding on
a different table and is fixed on the audit-mediums branch / PR #27; once that lands on
main this branch's alembic check is fully clean.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant