Questarr

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:

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 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:

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 (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:

Moderate and Low findings do not block release.

1.4 License policy

Questarr is licensed under GPL-3.0-only (see COPYING). 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:

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:

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 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:

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 runs an OWASP ZAP baseline scan against the production build on every push to main/release/*, weekly, and on manual dispatch (see the dynamic_analysis 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):

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):

  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 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 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).