Skip to content

fix(deploy): disable service account token automount (KSV-0036) - #132

Merged
seonghobae merged 3 commits into
mainfrom
claude/ksv-pattern-recognition-7x78ub
Aug 30, 2026
Merged

fix(deploy): disable service account token automount (KSV-0036)#132
seonghobae merged 3 commits into
mainfrom
claude/ksv-pattern-recognition-7x78ub

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Why

The prior deployment-hardening pass (#14) closed AVD-KSV-0020 (non-root runAsUser/runAsGroup), AVD-KSV-0021 (dedicated namespace), and documented the accepted AVD-KSV-0125 (trusted-registry image) suppression in .trivyignore.

One well-known Kubernetes Security (KSV) misconfiguration pattern was left unaddressed: AVD-KSV-0036 — ServiceAccountToken Automount Not Disabled. The gateway Deployment in deploy/kubernetes/waf-ids-ai-soc.yaml never talks to the Kubernetes API, so it has no need for the default ServiceAccount's token to be automounted into the container filesystem.

While validating this branch's CI, the required rust check (cargo test --locked --workspace) was reliably failing on tests/binary.rs::binary_serves_then_shuts_down_on_sigterm (run 99248003533) — the gateway exited with raw signal 15 instead of code 0. Root-caused and fixed in the second commit (see below); this was blocking the KSV fix from reaching a green head, so it's included in the same PR.

What

  • deploy/kubernetes/waf-ids-ai-soc.yaml: set spec.template.spec.automountServiceAccountToken: false on the gateway pod template.
  • src/main.rs: shutdown_signal() was an async fn, so tokio::signal::unix::signal() (the call that installs the OS-level SIGTERM handler) only ran once the future was first polled — inside axum::serve(...).with_graceful_shutdown(shutdown), i.e. after run_from_env had already bound the listener and printed the readiness line. A SIGTERM delivered in that window (exactly what the e2e test, and any container runtime with a short terminationGracePeriodSeconds, sends right after startup) fell through to the default disposition and killed the process instead of triggering graceful shutdown. Split registration (now synchronous, in install_shutdown_signal()) from waiting, and call it before run_from_env runs at all.

Verification

  • python3 -c "import yaml; list(yaml.safe_load_all(open('deploy/kubernetes/waf-ids-ai-soc.yaml')))" → parses cleanly.
  • cargo fmt --check → clean.
  • cargo test -p waf-ids-ai-soc --test binary → was reliably failing before the second commit, passes 5/5 after.
  • cargo test --locked --workspace → all tests pass except load_surfaces_state_rewrite_failures, which fails identically on main (unrelated pre-existing root-sandbox permission-check issue already tracked by open PR test(persistence): replace permission-based fault injection with a deterministic seam #93) and is unaffected by this change.
  • cargo clippy --locked --workspace --all-targets -- -D warnings → clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 보안 개선

    • 애플리케이션 파드에 서비스 계정 토큰이 자동으로 마운트되지 않도록 설정하여 불필요한 권한 노출 위험을 줄였습니다.
  • 버그 수정

    • 애플리케이션 시작 직후 종료 신호가 발생해도 정상적으로 처리되도록 개선했습니다.
    • 서비스가 준비 상태를 알리기 전에 종료 신호를 놓치거나 비정상 종료될 가능성을 줄였습니다.

The gateway pod never calls the Kubernetes API, so the default
ServiceAccount token should not be automounted into the container
filesystem. This is the one Trivy Kubernetes Security (KSV) misconfig
pattern the prior deployment-hardening pass (KSV-0020/KSV-0021/
KSV-0125, PR #14) left unaddressed.
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 53 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a23477ec-d67e-4de4-a0e5-27792e1128ca

📥 Commits

Reviewing files that changed from the base of the PR and between be45e69 and e67a939.

📒 Files selected for processing (1)
  • src/main.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 741c9d85-dfbd-47e0-9d75-fa80d2306499

📥 Commits

Reviewing files that changed from the base of the PR and between 1071176 and be45e69.

📒 Files selected for processing (2)
  • deploy/kubernetes/waf-ids-ai-soc.yaml
  • src/main.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Deployment 파드의 서비스 계정 토큰 자동 마운트를 비활성화했습니다. 애플리케이션은 실행 전에 종료 신호 핸들러를 설치합니다. Unix에서는 SIGTERM 등록을 동기적으로 수행하고, non-Unix에서는 Ctrl-C 대기를 반환된 Future에서 수행합니다.

Changes

파드 보안 설정

Layer / File(s) Summary
서비스 계정 토큰 자동 마운트 비활성화
deploy/kubernetes/waf-ids-ai-soc.yaml
Deployment 파드 spec에 automountServiceAccountToken: false를 추가했습니다.

종료 신호 초기화

Layer / File(s) Summary
종료 신호 핸들러 사전 설치
src/main.rs
mainrun_from_env 호출 전에 install_shutdown_signal()을 호출하도록 변경했습니다. Unix 구현은 SIGTERM 핸들러를 즉시 등록하고, non-Unix 구현은 반환된 Future에서 tokio::signal::ctrl_c()를 대기합니다.

Estimated code review effort: 2 (간단) | ~10 minutes

Merge Risk: ⚪ Minimal · up to be45e

The PR disables unnecessary service-account token automounting and installs shutdown handling before readiness, with no actionable merge-blocking risk remaining after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 ServiceAccount 토큰 자동 마운트 비활성화 변경을 정확히 설명합니다. 이는 실제 변경 사항 중 하나이지만 SIGTERM 처리 수정은 포함하지 않습니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (1 skipped: 1 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ksv-pattern-recognition-7x78ub

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

tests/binary.rs::binary_serves_then_shuts_down_on_sigterm was failing
CI on this branch (job 99248003533): the gateway exited with raw
signal 15 instead of code 0.

Root cause: shutdown_signal() was an async fn, so
tokio::signal::unix::signal() (which installs the OS-level handler)
only ran once the future was first polled inside
axum::serve(...).with_graceful_shutdown(shutdown) -- which happens
after run_from_env has already bound the listener and printed the
readiness line. A SIGTERM delivered in that window (exactly what the
e2e test, and any container runtime with a short
terminationGracePeriodSeconds, does right after startup) fell through
to the default disposition and killed the process instead of
triggering a graceful shutdown.

Fix: split registration from waiting. install_shutdown_signal() calls
tokio::signal::unix::signal() synchronously and returns only the
subsequent .recv() as a future, so main() installs the handler before
run_from_env runs at all.

Verified: `cargo test -p waf-ids-ai-soc --test binary` was reliably
failing before this change and passes 5/5 after.
@seonghobae
seonghobae marked this pull request as ready for review August 30, 2026 11:24
devin-ai-integration[bot]

This comment was marked as resolved.

Devin Review flagged (PR #132, src/main.rs:29-32) that the previous
commit only fixed the readiness/signal-registration race on the Unix
path: the non-Unix install_shutdown_signal() still wrapped
tokio::signal::ctrl_c() in an async block, so registration stayed
lazy and an immediate Ctrl-C could still kill the process before the
handler was installed.

tokio::signal::windows::ctrl_c() (unlike the cross-platform
tokio::signal::ctrl_c() convenience fn) registers synchronously and
returns a CtrlC handle, exactly mirroring unix::signal()/Signal, so
apply the same split-registration-from-waiting fix there.

Not covered by tests/binary.rs (its Windows test only exercises a
forced kill, not graceful Ctrl-C), and this sandbox has no Windows
cross-compiler to build/run against; verified the API shape directly
against the vendored tokio 1.53.1 source
(signal/windows.rs: `pub fn ctrl_c() -> io::Result<CtrlC>`,
`pub async fn recv(&mut self) -> Option<()>`), which matches the
already-used unix::Signal shape exactly.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 0 new potential issues.

Devin Review

seonghobae pushed a commit that referenced this pull request Aug 30, 2026
This branch forked from main before #132's shutdown-handler fix
merged, so its copy of src/main.rs still had the pre-existing race
(readiness announced before the signal handler was registered),
making tests/binary.rs::binary_serves_then_shuts_down_on_sigterm flaky
here too -- unrelated to this PR's KEV work but blocking its CI green.
Ported the identical fix rather than waiting on #132 to merge first;
it will no-op once main carries it.

Copy link
Copy Markdown
Contributor Author

Status on the two still-red required checks (head e67a939), both external/infra, not this PR's diff:

  • strix — the org's contextual-orchestrator-sidecar LLM security-scan job. Root-caused via job logs, not a flake: this run timed out reaching its own local sidecar (curl: (28) Operation timed out after 120002ms, gateway preflight request could not reach the local sidecar) after provider_discovery_failed provider=bytez code=http_status_500; an earlier run on this same branch failed differently (litellm.APIError: OpenrouterException — Invalid URL, 502). Two distinct transient infra failures from the same sidecar in one branch's lifetime — this is an org-side reliability problem with that workflow, not something in deploy/kubernetes/waf-ids-ai-soc.yaml or src/main.rs. It lives in ContextualWisdomLab/.github, which this session has read-only access to (no push, no PR/API access) — outside what I can fix from here.
  • opencode-review — a required check that only passes once the opencode-agent bot posts an APPROVED/CHANGES_REQUESTED review on this exact head SHA; the job itself just queries for that review and fails when none exists yet (No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head). Genuinely pending on that bot's dispatch, not a finding to act on.

Everything else is green: rust, trivy-fs, CodeQL, osv-scan/osv-scanner, dependency-review, both Semgrep jobs, all 4 fuzz targets, noema-review, coverage jobs. Will keep watching.


Generated by Claude Code

@seonghobae
seonghobae merged commit 8319bed into main Aug 30, 2026
33 of 35 checks passed
@seonghobae
seonghobae deleted the claude/ksv-pattern-recognition-7x78ub branch August 30, 2026 13:03
seonghobae added a commit that referenced this pull request Aug 31, 2026
…s (KEV) catalog (#133)

* feat(threat-intel): add CISA KEV catalog ingestion adapter

Adds a new threat-intel adapter alongside the existing STIX/MISP/TAXII/
OpenCTI family: POST /api/threat-intel/cisa-kev fetches the CISA Known
Exploited Vulnerabilities catalog (a single well-known public JSON feed,
https://www.cisa.gov/known-exploited-vulnerabilities-catalog) and upserts
one `cve`-typed ThreatIndicator per entry, keyed by CVE ID, severity
escalated to Critical when CISA has tied the CVE to a known ransomware
campaign (High otherwise — mere inclusion in the catalog already signals
CISA-confirmed active exploitation).

- src/kev_import.rs: pure parser (`parse_kev_document`), mirroring the
  misp_import/opencti_import/stix_import module shape exactly. Accepts
  the real catalog shape (`{"vulnerabilities": [...]}`) or a bare array.
  KEV entries carry no IP/domain/URL/hash observable, so `dnsbl` stays
  empty (kept only for ThreatFeedImport parity).
- src/lib.rs: `import_kev_feed` handler follows the phishing-database
  fetch-by-URL pattern (not the MISP/STIX/OpenCTI paste-a-document
  pattern) since KEV has one canonical, stable, publicly known URL that
  every consumer wants pulled automatically rather than hand-relayed.
  Reuses the existing `fetch_text_feed`/`apply_threat_feed_import`
  plumbing untouched. `validate_http_url` gained a third `allowed_hosts`
  parameter (was hardcoded to the phishing-database allowlist) so KEV
  gets its own `www.cisa.gov`-only default, matching the SSRF-safety
  posture phishing-database already has (opt-out via
  `allow_non_default_hosts`); all other call sites updated.
- crates/waf-ids-core: new buyer-evidence-manifest endpoint entry.
- docs/architecture.md, docs/security/compliance-mapping.md: documented
  alongside the other threat-intel adapters.

Deliberately not adding a cargo-fuzz target: none of the five existing
JSON-import adapters (misp_import, opencti_import, stix_import,
suricata_eve, coraza_audit) have one today — CLAUDE.md's fuzzed-surface
list (request scorer, state deserializer, admin-token parser, DNSBL zone
export) doesn't cover this adapter family. Matched the existing
convention instead: a `parse_never_panics_on_arbitrary_text` unit test,
same as its siblings.

Verified: cargo fmt --check, cargo test --locked --workspace (all pass
except the pre-existing root-sandbox-only `load_surfaces_state_rewrite_failures`
flake tracked by PR #93, unrelated to this change), cargo clippy
--locked --workspace --all-targets -- -D warnings.

* fix(waf): never content-match CVE threat indicators in scoring

Devin Review flagged (PR #133): every KEV-imported CVE indicator was
entering score_request's generic substring matcher at High/Critical
severity. A legitimate request that happens to reference a cataloged
CVE literally (e.g. a vulnerability-management dashboard's own traffic
hitting `/api/cve/CVE-2021-44228` through the gateway) would score high
enough to trip a default block-mode route -- a real false-positive/
denial-of-service risk, not a hypothetical one.

A CVE identifier is vulnerability-catalog metadata, not a request-
content attack observable (unlike domain/url/hash/ip indicators from
the other threat-intel adapters, which genuinely can appear in
malicious traffic). Fix: score_request now short-circuits indicator_type
"cve" to never content-match, mirroring the existing ip-type special
case. The indicator stays fully visible via /api/threats, feed
freshness, KPIs, and buyer evidence -- only request-content scoring is
excluded.

Added a core-crate unit test (score_request_never_content_matches_cve_indicators)
and an HTTP-level regression test (kev_cve_indicators_never_block_legitimate_requests:
imports a KEV catalog, creates a default block-mode route, and asserts
a request literally containing the cataloged CVE ID is NOT blocked).
Verified both fail without the fix and pass with it.

* fix(shutdown): port SIGTERM/Ctrl-C race fix from PR #132

This branch forked from main before #132's shutdown-handler fix
merged, so its copy of src/main.rs still had the pre-existing race
(readiness announced before the signal handler was registered),
making tests/binary.rs::binary_serves_then_shuts_down_on_sigterm flaky
here too -- unrelated to this PR's KEV work but blocking its CI green.
Ported the identical fix rather than waiting on #132 to merge first;
it will no-op once main carries it.

* docs(research): ground CISA KEV severity policy in EPSS/BOD literature

Devin Review flagged (PR #133) that this substantive feature PR was
missing the research grounding AGENTS.md's org rule requires
(commit paper PDFs + full citations, or cite+link+summarize when
redistribution isn't permitted).

Added a "Further reading" section to docs/architecture.md (matching
docs/fuzzing.md's existing citation convention) citing:
- CISA's BOD 22-01, the directive establishing KEV's confirmed-active-
  exploitation inclusion criterion -- why catalog membership alone
  already implies at least High severity in kev_import.rs.
- Jacobs et al. (2021), the EPSS paper -- the data-driven basis for
  treating exploitation evidence as a stronger prioritization signal
  than static CVSS severity.
- Shimizu & Hashimoto (2025, arXiv:2506.01220, CC BY 4.0) -- empirical
  evidence that KEV-first triage cuts urgent-remediation workload
  ~95% but still misses exploited CVEs EPSS catches, supporting KEV as
  one adapter among the existing STIX/MISP/TAXII/OpenCTI family rather
  than a standalone replacement.

Committed the arXiv PDF (CC BY 4.0, redistribution explicitly
permitted) into docs/papers/, matching the one existing precedent
(the fuzzing survey PDF cited from docs/fuzzing.md). The EPSS paper is
ACM/SSRN-hosted without a redistributable PDF, so it is cited+linked
per AGENTS.md's explicit fallback instead of attached.

* fix(security): don't route request-supplied kev_url into the fetch by default

CodeQL flagged (PR #133, critical severity, rust/request-forgery at
src/lib.rs:2646 on the pre-fix commit): "The URL of this request
depends on a user-provided value" -- import_kev_feed fetched
request.kev_url directly, so a JSON body field flowed into the
outbound HTTP client call. validate_kev_import_request already
allowlists the host (www.cisa.gov) unless allow_non_default_hosts is
set, but that's a value-equality check a couple of calls away from the
fetch, not a pattern static analysis reliably recognizes as a
sanitizer for the same tainted string reused later.

Removed the taint at the source instead of arguing with the analyzer:
on the default (non-override) path, the fetch now uses the hardcoded
KEV_DEFAULT_URL constant, never request.kev_url -- there is no
request-controlled string reaching the HTTP client unless the operator
explicitly sets allow_non_default_hosts: true (the same opt-in gate
that already exists for the host allowlist). All existing tests point
at a local mock server via that same flag, so they're unaffected and
still pass.

Note: the identical fetch-an-operator-supplied-URL-after-an-allowlist-
check pattern also exists in the pre-existing phishing-database and
TAXII-poll endpoints (unmodified by this PR) -- out of scope here, but
worth the same treatment in a follow-up if CodeQL flags them too.

* fix(kev): stop validating kev_url when it won't be used

Devin Review flagged (PR #133): with allow_non_default_hosts false,
validate_kev_import_request still checked kev_url against the CISA
host allowlist even though import_kev_feed (65d60a9) now always
fetches the hardcoded default in that mode -- so a validated
same-host custom path was silently discarded, and the validation
itself was misleading (accept-then-ignore).

Chose Devin's second suggested option over restoring same-host
customization: honoring a validated-but-still-request-controlled URL
on the default path is exactly the pattern that triggered the
original CodeQL SSRF alert, so reintroducing it isn't a real fix.
Instead, kev_url is now only validated when allow_non_default_hosts
is true (the one mode where it's actually fetched) -- and once
opted in, any well-formed http(s) URL is accepted with no host
restriction, matching how that same flag already works for
phishing-database and TAXII. KEV_ALLOWED_HOSTS is removed as dead
code; there is no longer a partial-trust "same host, no opt-in"
tier to enforce.

Replaced the now-invalid kev_feed_import_rejects_disallowed_host_by_default
HTTP test (there is no more "disallowed host" rejection under the new
contract) with a validate_kev_import_request unit test covering both
modes directly.

* fix(security): remove kev_url from the request contract entirely

CodeQL's critical SSRF alert (rust/request-forgery) was still firing
on the previous fix (65d60a9): gating the tainted request.kev_url
behind an allow_non_default_hosts runtime flag doesn't register as a
sanitizer to CodeQL's dataflow analysis -- the string still
originates from the HTTP request body and still reaches
fetch_text_feed on that code path, so the alert (rightly, from a
pure taint-tracking standpoint) persisted.

Rather than keep trying to convince the analyzer a runtime guard is
safe, removed the taint source outright: kev_url and
allow_non_default_hosts are gone from KevImportRequest entirely.
import_kev_feed always fetches AppState::kev_catalog_url, which is
deployment-time config only -- set via the new KEV_CATALOG_URL env
var (validated at startup, alongside run_from_env's other env
parsing) or AppState::with_kev_catalog_url() in tests -- never
sourced from a client request. There is now no code path in this
handler where request-supplied data reaches an outbound fetch at
all.

This is a stronger fix than the previous one on the merits too, not
just for the analyzer: KEV genuinely has one canonical, stable,
government-published URL, so per-request override was never load-
bearing functionality, just a testing convenience that's now served
by the AppState builder instead.

Updated the three affected tests to point AppState::kev_catalog_url
at a local mock server instead of the request body, and replaced the
now-obsolete kev_url validation test with a plain feed-metadata
validation test. CLAUDE.md's Runtime Configuration section documents
the new env var.

* fix(kev): decouple KEV catalog fetch from the shared request-URL sink

fetch_text_feed's url parameter is fed by phishing-database's
request-supplied domain_url/ip_url (a pre-existing, admin-gated
"fetch from an operator-chosen URL" design on main). Routing the KEV
catalog pull through that same shared function -- confirmed via diff
against origin/main that fetch_text_feed/validate_http_url were
otherwise byte-identical -- was enough for CodeQL's rust/request-forgery
query to keep flagging the shared sink as newly touched by this PR,
even after KEV_CATALOG_URL was made config-only.

Give KEV its own fetch_kev_catalog function (same pattern already used
by fetch_taxii_objects) so the config-only path never shares a
function with the request-URL adapters, and revert validate_http_url
to its original two-argument form now that KEV no longer needs a
custom allowed-hosts list.

Also add KEV_CATALOG_URL to clear_run_env: Devin Review flagged that
an inherited value would leak across run_from_env tests, since the
list omitted it while validating every other run_from_env env var.

* fix(kev): restrict KEV_CATALOG_URL to CISA's own host

Devin Review flagged that KEV_CATALOG_URL had no host allowlist, so an
unintended inherited or misconfigured value could redirect this
privileged catalog fetch -- a real SSRF sub-pattern (env-var-sourced
URLs are externally influenceable via container/orchestrator
inheritance, not just via request bodies), which lines up with why
CodeQL kept flagging feed_http.get(url) here even after the previous
commit fully decoupled the fetch from any request-derived data.

Add validate_kev_catalog_url, enforced both at startup (fail fast on a
bad env var) and again immediately before the fetch: the host must be
www.cisa.gov or loopback (loopback only so tests can point it at a
local mock server), with no override -- unlike the operator-URL
adapters, KEV has no legitimate reason to fetch from anywhere else.

* docs(kev): correct stale "internal mirror" language for the new host allowlist

Devin Review caught that AppState::with_kev_catalog_url's doc comment
still promised support for pointing KEV_CATALOG_URL at an internal
mirror, which the host-allowlist fix in 3a47ce8 now rejects at startup
-- a real contract mismatch, not just stale wording. There was never
an actual mirror requirement (that phrase was leftover illustrative
language from an earlier draft); update the builder's doc comment and
CLAUDE.md's Runtime Configuration entry to describe the real contract
(CISA's own host, or loopback for local test mocking) rather than
inventing a mirror-allowlist feature nobody asked for.

* fix: validate CVE ID syntax on KEV import; scope Windows cfg precisely

CodeRabbit review, both real:

- kev_import.rs: any non-empty cveID (e.g. "not-a-cve") was accepted
  and stored as a cve threat indicator. Add is_valid_cve_id, checking
  CVE.org's CVE-<4-digit year>-<4+ digit sequence> syntax
  case-insensitively; entries that fail it are now Skipped like a
  missing cveID already was. Panic-safe on arbitrary catalog text via
  str::get instead of direct slicing (added a multi-byte-boundary case
  to parse_never_panics_on_arbitrary_text to cover it).

- main.rs: install_shutdown_signal's second definition was gated on
  cfg(not(unix)), but its body calls tokio::signal::windows::ctrl_c,
  which only exists on Windows -- any other non-Unix target would fail
  to compile there. Scope the cfg to windows specifically.

* fix(threat-feeds): don't reap indicators still owned by another feed

Devin Review flagged a real bug in the feed-ownership reconciliation
logic introduced by an external merge (192ffba, authored outside this
session): apply_threat_feed_import removed every key the refreshing
feed had previously owned from the global threats collection
unconditionally, without checking whether another feed's ownership
record still claimed the same indicator_type+value+source key. Two
feeds sharing a key (e.g. the same CVE imported under a shared
`source`) meant refreshing either one made the indicator vanish from
enforcement until the other feed's next refresh happened to re-add it.

Track "still owned by some other feed" via the current (post-replace)
ownership table and only reap a dropped key when no feed claims it any
more. Also switches previous_keys from a Vec to a HashSet, which
incidentally fixes a second (lower-severity) finding on the same
commit: the per-threat previous_keys.contains() scan was O(n) per
retained item, made O(1) here.

Added feed_refresh_preserves_indicators_still_owned_by_another_feed,
which fails against the pre-fix logic (the shared indicator disappears
after the first feed's refresh) and passes with it -- verified by
reasoning through both code paths.

Not actioned (informational/acceptable trade-offs, not bugs):
- "Upgraded feeds retain withdrawn indicators": state persisted before
  this feature existed has no ownership history, so the first refresh
  per feed after upgrade can't retroactively reconcile pre-existing
  drift. Self-heals from each feed's first post-upgrade refresh
  onward; there's no safe way to backfill ownership for data imported
  before it was tracked.
- "DNSBL refresh remains append-only": reconciliation covers threats
  only, matching the scope of the original finding this responds to
  (KEV catalogs carry no DNSBL entries). Extending it to DNSBL entries
  for the other four adapters is a follow-up, not a regression here.

* fix(kev): fail closed without write creds

* fix(kev): reject mostly-unparsable catalogs; scope credential source to admin creds

Devin Review, both real:

- kev_import.rs: apply_threat_feed_import now reconciles (a refresh
  treats keys missing from the new snapshot as withdrawn and removes
  them). kev_material_from_value's only acceptance bar was "at least
  one usable cveID," so a catalog that's mostly unparsable -- a fetch
  truncated mid-transfer, a CISA response format regression -- would
  have been accepted and, via reconciliation, read as a mass
  withdrawal of still-exploited CVEs instead of the bad fetch it
  actually was. Require a real majority of entries to have parsed
  (skipped_entries <= threats.len()) before trusting a snapshot as
  authoritative. Adjusted skips_entries_with_malformed_cve_id's fixture
  to stay under the new bar and added
  rejects_catalog_where_most_entries_are_unparsable.

- credentials.rs: bootstrap_secrets set the registry-wide
  CredentialSource to File whenever ANY key -- including a file that
  supplies only kev_catalog_url -- came from the credentials file, even
  when ADMIN_TOKEN/ADMIN_TOKENS actually came from env. CredentialSource
  is documented and reported via HealthStatus/support bundle as admin
  credential provenance specifically, so this misreported security-
  relevant operational state. Track admin-credential file/env
  provenance independently of kev_catalog_url's, matching the
  documented contract. Added
  file_only_kev_catalog_url_does_not_misreport_env_admin_token_as_file_backed.

Not actioned in this commit -- flagging separately for the user's
scope decision: Devin also noted that apply_threat_feed_import's
reconciliation now applies uniformly to all five existing feed
adapters (STIX/MISP/TAXII/OpenCTI/phishing-database), not just KEV,
which changes their prior upsert-only semantics; anyone relying on
incremental/partial imports under a reused feed_id across the other
four adapters would now see earlier entries treated as withdrawn. This
is a cross-adapter behavior/policy question outside what a bug fix can
resolve unilaterally.

* fix(kev): remove runtime catalog URL override

* fix(kev): preserve operator indicators on feed refresh

* fix(kev): preserve operator-owned threat payloads

* fix(threat-feeds): report actual upserted count, not submitted count

Devin Review: apply_threat_feed_import's operator-ownership skip
(8b7954d) omits operator-owned threats from the upsert loop, but
ThreatFeedImportResult.upserted_threats still reported feed.threats.len()
-- the full submitted set, including entries that were skipped. Import
clients (including KEV's response) received an inflated success count
whenever a feed overlapped operator-managed data.

Count actual upserts in the loop and return that. ThreatFeedStatus.threat_count
is left as feed.threats.len() deliberately -- it represents catalog
membership (what this feed's snapshot claims), not applied mutations,
per Devin's suggested distinction.

Added import_result_excludes_operator_owned_threats_from_upserted_count.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.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.

2 participants