Skip to main content

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:
  • Dependabot alerts — GitHub’s continuous vulnerability scanning against package-lock.json, configured in .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 on every push and pull request against main/release/*, and again as a release gate in .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: 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:
  • 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, 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). 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 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), and default setup requires no workflow-file maintenance as CodeQL/query versions evolve.
  • Semgrep runs as the semgrep job in .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): 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:
  • The semgrep job (.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.:
    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 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): 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: 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).