> ## Documentation Index
> Fetch the complete documentation index at: https://docs.questarr.app/llms.txt
> Use this file to discover all available pages before exploring further.

# VULNERABILITY MANAGEMENT

# Vulnerability Management: SCA, SAST & DAST

This document defines Questarr's policy for handling findings from Software
Composition Analysis (SCA — third-party dependency vulnerabilities and
licenses), Static Application Security Testing (SAST — first-party code
analysis), and Dynamic Application Security Testing (DAST — vulnerabilities
observed by exercising a running instance of the app). It covers how findings
are identified, prioritized, remediated, and enforced before release.

This is a policy document, distinct from two related documents:

* [`docs/SECURITY_ASSESSMENT.md`](/docs/SECURITY_ASSESSMENT.md) — the risk
  register of specific, already-known architectural/design risks.
* [`docs/SECURITY.md`](SECURITY.md) — vulnerability
  disclosure process and deployment hardening guide.

This document instead answers: when an SCA, SAST, or DAST *tool* reports a
new finding, what happens next, and how fast.

## 1. Software Composition Analysis (SCA)

SCA covers vulnerabilities in third-party npm dependencies and the licenses
those dependencies are distributed under. See
[`docs/DEPENDENCIES.md`](/docs/DEPENDENCIES.md) for how dependencies are
selected and tracked; this section covers what happens once a vulnerability
or license problem is found in one.

### 1.1 Identification

Findings are surfaced through two automated channels, both required, not
optional add-ons:

* **Dependabot alerts** — GitHub's continuous vulnerability scanning against
  `package-lock.json`, configured in
  [`.github/dependabot.yml`](/.github/dependabot.yml). Runs continuously
  and on every dependency-graph update, in addition to the weekly update PRs.
* **`npm audit`** — run as the `sca-scan` job in
  [`.github/workflows/ci.yml`](/.github/workflows/ci.yml) on every push and
  pull request against `main`/`release/*`, and again as a release gate in
  [`.github/workflows/deploy.yml`](/.github/workflows/deploy.yml) (see
  §1.3). This catches vulnerabilities in the exact resolved dependency tree
  at build time, independent of Dependabot's scan cadence.
* **License check** — `license-checker` runs in the same `sca-scan` job
  against every production dependency to catch newly introduced disallowed
  licenses (see §1.4).

### 1.2 Prioritization & remediation thresholds

Vulnerability findings are triaged by severity (npm audit / GitHub Advisory
Database CVSS rating, `Critical` / `High` / `Moderate` / `Low`). Each
severity has a maximum time-to-remediation, measured from when the finding
first appears:

| Severity | Remediation SLA | Notes |
| - | - | - |
| Critical | 7 days | Patch, upgrade, or apply a documented `overrides` pin immediately; treat as release-blocking (§1.3). |
| High | 30 days | Same remediation paths as Critical; release-blocking (§1.3). |
| Moderate | 90 days | Bundled into the next routine Dependabot update cycle unless actively exploited, in which case treat as High. |
| Low | Next scheduled dependency update | No dedicated action required; fixed opportunistically via Dependabot's weekly PRs. |

Remediation, in order of preference:

1. **Upgrade** the dependency (direct or transitive, via Dependabot PR or
   manual `npm install`) to a patched version.
2. **Pin via `overrides`** in `package.json` when the vulnerable package is a
   transitive dependency and no direct upgrade path exists yet (see
   `docs/DEPENDENCIES.md`).
3. **Accept the risk**, only when neither of the above is possible (e.g. no
   fix released yet and the vulnerable code path is unreachable in Questarr's
   usage). Acceptance must be recorded as a `not_affected` statement in the
   [VEX feed](/docs/VEX.md) (`security/vex/questarr.openvex.json`) with a
   justification tied to the actual code path, so it's visible and revisited
   rather than silently suppressed.

### 1.3 Pre-release gate

**Every release must be clean of unresolved Critical/High SCA findings.**
This is enforced by an automated status check, not a manual step:

* The `sca-scan` job (`.github/workflows/ci.yml`) runs
  `node scripts/audit-prod.mjs --audit-level=high` (a wrapper around
  `npm audit --omit=dev` that honours the VEX feed) on every push and pull
  request targeting `main`/`release/*`. A Critical or High finding in
  production dependencies that the VEX feed does not mark `not_affected` or
  `fixed` fails the job and blocks merge.
* The same check runs again as a required job in
  [`.github/workflows/deploy.yml`](/.github/workflows/deploy.yml), ahead of
  `build-and-push`, so a Docker image cannot be published while a Critical or
  High vulnerability is present in the dependency tree — independent of
  whether `main` was already gated at merge time (e.g. a new advisory
  published after merge, before release).
* The only way to release with a known Critical/High finding open is the
  documented risk-acceptance path in §1.2 (item 3), which requires a
  reviewed `not_affected` statement in the VEX feed — never a silent
  `--force` or skipped check.

Moderate and Low findings do not block release.

### 1.4 License policy

Questarr is licensed under `GPL-3.0-only` (see [`LICENSE`](/LICENSE)).
Dependency licenses are checked by the `sca-scan` CI job using
`license-checker` against an explicit allow-list; anything outside it fails
the build and must be triaged before merge:

* **Allowed**: MIT, ISC, BSD-2-Clause, BSD-3-Clause, Apache-2.0, CC0-1.0,
  0BSD, Python-2.0, BlueOak-1.0.0, OFL-1.1, Unlicense, and copyleft licenses
  compatible with distributing Questarr under `GPL-3.0-only` (GPL-3.0,
  LGPL-2.1, LGPL-3.0, MPL-2.0). GPLv2-only is deliberately excluded: a
  GPLv2-only dependency isn't combinable into a GPLv3-only distribution
  unless it's dual-licensed or carries an "or later" clause, so it needs a
  reviewed exception rather than a blanket allow.
* **Disallowed / requires manual review before use**: any license not on the
  allow-list, including unrecognized/custom licenses, `UNLICENSED`,
  source-available-but-restricted licenses (e.g. Commons Clause, BUSL,
  SSPL), and packages with no declared license. A disallowed license found
  in a new or updated dependency blocks the introducing PR until the
  dependency is replaced or an explicit, documented exception is added to
  the `license-checker` allowlist with maintainer sign-off.

## 2. Static Application Security Testing (SAST)

SAST covers vulnerabilities in Questarr's own source code (`client/`,
`server/`, `shared/`), as opposed to third-party dependency code (§1).

### 2.1 Identification

Two SAST layers run, deliberately kept separate rather than merged into one
tool/workflow:

* [CodeQL](https://codeql.github.com/) analysis runs via GitHub's **default
  setup** (Settings → Code security → Code scanning), covering JavaScript/
  TypeScript (client + server) sources on every pull request, push to
  `main`, and a periodic background schedule GitHub manages automatically.
  Default setup is deliberately used instead of a custom `codeql.yml`
  workflow: GitHub does not allow a repository to run both at once (SARIF
  uploads from a custom/"advanced" CodeQL workflow are rejected outright
  while default setup is enabled — see the note in
  [`docs/SECURITY_ASSESSMENT.md`](/docs/SECURITY_ASSESSMENT.md)), and
  default setup requires no workflow-file maintenance as CodeQL/query
  versions evolve.
* [Semgrep](https://semgrep.dev/) runs as the `semgrep` job in
  [`.github/workflows/sast.yml`](/.github/workflows/sast.yml) on every push
  and pull request targeting `main`/`release/*`. This is a separate SARIF
  producer (tool name `Semgrep`, not `CodeQL`), so it doesn't collide with
  CodeQL default setup, and it exists specifically to give SAST a per-PR
  pass/fail signal (§2.3) that default setup alone cannot provide.

Both upload their SARIF output to the repository's Security → Code scanning
alerts tab, which is the source of truth for open SAST findings regardless
of which tool produced them.

### 2.2 Prioritization & remediation thresholds

Findings are triaged by CodeQL's reported severity
(`Critical` / `High` / `Medium` / `Low`, alert-native "Error"/"Warning"/"Note"
mapped accordingly):

| Severity | Remediation SLA | Notes |
| - | - | - |
| Critical | 7 days | Fix or explicitly suppress with justification (see below); treat as release-blocking. |
| High | 30 days | Same as Critical. |
| Medium | 90 days | Scheduled into normal development; re-evaluate severity if exploitability changes. |
| Low | Best-effort, no fixed deadline | Track in the code scanning tab; fix opportunistically. |

Remediation process, per finding:

1. **Triage** — a maintainer reviews the alert in the Security tab within 5
   business days of it appearing, to confirm severity and exploitability in
   Questarr's context (self-hosted, single/few-user app — see
   [`docs/THREAT_MODEL.md`](/docs/THREAT_MODEL.md) for the applicable trust
   model).
2. **Fix** — the underlying code is changed to eliminate the flagged pattern
   (preferred outcome for all severities).
3. **Dismiss with reason** — if a finding is a false positive or genuinely
   not exploitable given Questarr's architecture, it is dismissed directly
   in the Code scanning UI with a reason and comment, not silently ignored.
   A pattern of dismissals in the same area should prompt a query-suppression
   review rather than repeated one-off dismissals.

### 2.3 Pre-merge gate

CodeQL's default setup (§2.1) intentionally has no per-PR blocking signal —
alert triage there follows the SLA table above and periodic maintainer
review, since it requires human judgment about exploitability that a hard
gate would short-circuit.

Semgrep closes that gap with an actual merge-blocking check:

* The `semgrep` job ([`.github/workflows/sast.yml`](/.github/workflows/sast.yml))
  runs `semgrep scan` with the `p/security-audit`, `p/secrets`,
  `p/owasp-top-ten`, `p/javascript`, `p/typescript`, and `p/react` rulesets,
  unfiltered by severity, writing both a JSON and a SARIF copy of every
  finding. The SARIF copy is uploaded to the Code scanning tab as-is, so
  `WARNING`/`INFO` findings stay visible there (matching the Medium/Low rows
  in §2.2) instead of being dropped from the scan entirely. A separate step
  then reads the JSON output and fails the job — blocking merge — if any
  `ERROR`-severity finding is present; this dedicated step is the required
  status check for OSPS-VM-06.02, kept independent of the scan step so the
  blocking condition can never silently filter what reaches Code Scanning.
* **Suppression (declaring a finding non-exploitable):** a blocking finding
  may be suppressed only via an inline `// nosemgrep: <rule-id>` comment on
  the line immediately preceding the flagged code (or on the same line —
  Semgrep does not recognize the directive anywhere else), with a mandatory
  trailing justification, e.g.:

  ```ts theme={null}
  // nosemgrep: javascript.lang.security.audit.path-traversal -- path is validated against an allow-list in server/middleware.ts:293 before this line
  ```

  A suppression without a stated reason is not acceptable and should be
  rejected in review. Suppressions are visible in the diff, so they go
  through the same PR review as any other change — no separate approval
  channel exists for dismissing a finding, unlike CodeQL's Code Scanning UI
  dismissal path (§2.2, item 3).

## 3. Dynamic Application Security Testing (DAST)

DAST covers vulnerabilities observed by exercising a running instance of
Questarr, as opposed to reading its source (§2) or its dependency tree (§1).
This is a newer control than §1/§2 — see §3.4 for what's not yet built out.

### 3.1 Identification

The `zap-baseline` job in
[`.github/workflows/dast.yml`](/.github/workflows/dast.yml) runs an
[OWASP ZAP](https://www.zaproxy.org/) baseline scan against the production
build on every push to `main`/`release/*`, weekly, and on manual dispatch
(see the [`dynamic_analysis`](/docs/BEST_PRACTICES.md) evidence for how the
target is booted). Each run's HTML/JSON/MD report is uploaded as the
`zap-baseline-report` workflow artifact.

### 3.2 Prioritization & remediation thresholds

ZAP classifies each alert with its own `Risk` rating (`High` / `Medium` /
`Low` / `Informational`), which this policy treats as the CVSS-qualitative
equivalent for triage purposes (`High` ≈ CVSS High/Critical, `Medium` ≈ CVSS
Medium, satisfying the "medium or higher" threshold in
[`dynamic_analysis_fixed`](/docs/BEST_PRACTICES.md)):

| Risk | Remediation SLA | Notes |
| - | - | - |
| High | 30 days | Fix the underlying cause (code/config change); treat as release-blocking once §3.4's gate lands. |
| Medium | 90 days | Same triage rigor as High; scheduled into normal development. |
| Low | Best-effort, no fixed deadline | Track via the artifact; fix opportunistically. |
| Informational | No deadline | Reviewed for context, not treated as a vulnerability on its own. |

Remediation process, per finding, once confirmed exploitable in Questarr's
context (self-hosted, single/few-user app — see
[`docs/THREAT_MODEL.md`](/docs/THREAT_MODEL.md)):

1. **Fix** — change the code/config that causes the alert (preferred outcome
   for all severities).
2. **Accept as a false positive / non-exploitable** — record the reasoning
   in the PR or a `docs/SECURITY_ASSESSMENT.md` risk-register entry if the
   alert recurs across scans, rather than letting it sit unexplained in the
   artifact scan after scan.

### 3.3 First scan and current enforcement status

The first completed `dast.yml` run (2026-07-14, triggered by the merge that
introduced this workflow) found **0 FAIL-level (High) alerts** and **5
WARN-level alerts**, all Low or Informational under ZAP's own default risk
ratings except one Medium (`CSP: Wildcard Directive`). Per §3.2's SLA that
Medium finding was fixed immediately rather than run out its 90-day window:

| Rule ID | Alert | Risk | Disposition |
| - | - | - | - |
| 10055 | CSP: Wildcard Directive | Medium | **Fixed.** Helmet's default `font-src`/`style-src` allow any `https:` origin; Questarr's fonts/styles are all self-hosted, so both are narrowed to `'self'` (plus `data:` for fonts, `'unsafe-inline'` kept for styles since several components render inline `style` attributes) (`server/routes.ts:360-361`). |
| 10063 | Permissions Policy Header Not Set | Low | **Fixed.** Helmet dropped Permissions-Policy support; added directly (`server/routes.ts:371-377`), denying camera/microphone/geolocation/payment/USB/`interest-cohort`. |
| 10049 | Storable but Non-Cacheable Content | Low | Accepted — static SPA assets served with standard cache headers, not exploitable. `.zap/rules.tsv`. |
| 10109 | Modern Web Application | Info | Accepted — informational-only, not a vulnerability. `.zap/rules.tsv`. |
| 90004 | Cross-Origin-Embedder-Policy Header Missing | Low | Accepted — enabling COEP would break cross-origin IGDB/NexusMods image loading (see [`docs/SECURITY_ASSESSMENT.md`](/docs/SECURITY_ASSESSMENT.md) risk register). `.zap/rules.tsv`. |

With that triage done, `dast.yml` now runs with **`fail_action: true`** and
`rules_file_name: .zap/rules.tsv` — the three accepted findings above are
downgraded to `IGNORE` (with the reasoning inline in that file) so they don't
recur as false failures, while any *new* WARN or FAIL alert still fails the
build, matching the blocking behavior §1.3 and §2.3 already have for SCA/SAST.

**The gate immediately proved itself**: the next two scans (2026-07-14/15,
triggered by the merges that landed the fixes above and a follow-up test
refactor) both failed on `10055 — CSP: style-src unsafe-inline`. Rule 10055
covers more than the wildcard-directive check fixed above; `style-src` still
carries `'unsafe-inline'` (kept deliberately — several components render real
inline `style="..."` attributes, confirmed empirically with a Playwright
script that removed `'unsafe-inline'` and captured `securitypolicyviolation`
events: Radix's Drawer component injects a `<style>` block that fails to
apply without it, not just a theoretical concern). Removing it would break
rendering, not just tighten policy, per the same reasoning already recorded
for `script-src` and `font-src`/`style-src` above. This is a Low-risk,
already-reviewed tradeoff, not a new confirmed vulnerability, so it's added
to [`.zap/rules.tsv`](/.zap/rules.tsv) as `IGNORE` rather than reversing the
`server/routes.ts` decision.

**Known limitation of this fix — read before adding another entry here**:
`zap-baseline.py`'s `-c` config file (and the `zaproxy/action-baseline`
action's own report post-processing) both key `IGNORE`/`WARN`/`FAIL` purely
by ZAP plugin ID, with no way to distinguish individual alert messages under
the same plugin. `10055 IGNORE` therefore also silently suppresses the
`CSP: Wildcard Directive` alert already fixed above — if that regressed (e.g.
a future change reintroduced a bare `https:` scheme into any directive), the
DAST gate would no longer catch it. Rather than leave that gap unaddressed or
take on the much larger effort of eliminating `'unsafe-inline'` entirely
(replacing every inline `style` prop with a CSS-custom-property or nonce-based
approach — out of scope here), the wildcard-directive check now has a
dedicated compensating control: `server/__tests__/security.test.ts` asserts
directly that no CSP directive contains a bare `https:`/`http:`/`ftp:` scheme
token, verified to both pass on the current config and fail if the wildcard
is reintroduced. This is arguably more precise regression coverage than ZAP's
plugin-level grouping would have given anyway.

Kept here as a visible record that the newly-enabled gate does catch real
signal — it isn't a rubber stamp — and of the concrete limitation found and
compensated for in the process.

### 3.4 Planned follow-up

One gap remains: `allow_issue_writing` is still `false`, so a finding is only
visible in the `zap-baseline-report` artifact of the run that produced it,
rather than getting a persistent GitHub issue the way Code Scanning provides
for SAST/SCA (§1, §2). Turn it on once there's a reason to track a DAST
finding across multiple runs instead of fixing/accepting it immediately (as
happened with the first scan above).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.