Skip to content

Security Scanning

Last reviewed: 2026-06-23

Project NEXUS is a public AGPL repository. Security scanning must distinguish reachable production risk from development-tooling noise.

Reporting a Vulnerability

Do not open a public issue for an unpatched vulnerability. Use the private disclosure path documented in SECURITY.md.


Security Sources Of Truth

Document or file Purpose
SECURITY.md Public vulnerability disclosure policy and safe-research rules.
.github/workflows/security-scan.yml CI security scan workflow (runs nightly + on every push to main).
.github/workflows/dependency-review.yml Lightweight PR gate — runs only when package files change.
owasp-suppressions.xml OWASP Dependency-Check suppressions with documented reasons.
.trivyignore Trivy suppressions with documented reasons.
.semgrepignore Semgrep path exclusions (dead/legacy code).
composer.lock, package-lock.json, react-frontend/package-lock.json Dependency state that scanners evaluate.

CI Scan Coverage

The security-scan.yml workflow runs the following tools in order. The CI definition is the authoritative reference — this table is a summary only.

Tool What it covers Blocking?
composer audit --locked PHP CVEs in composer.lock Yes
Trivy filesystem (table) Filesystem CVEs at CRITICAL/HIGH Yes, respects .trivyignore
Trivy filesystem (SARIF) Same — uploads to GitHub Security tab No (visibility only)
Semgrep (SAST) PHP injection, secret patterns, security anti-patterns No (SARIF upload only)
TruffleHog Verified secrets in git history Yes
OWASP Dependency-Check Transitive CVEs across PHP + npm, CVSS ≥ 7 Yes, respects owasp-suppressions.xml
npm audit --omit=dev Production npm CVEs at high+ Yes
Trivy container scan OS/library CVEs inside the built Docker image Yes (push events only)

Full scan results land in the GitHub Security tab (SARIF uploads) and as workflow artifacts for the OWASP HTML report.


Running Routine Dependency Checks Locally

Run these before opening a PR when you have changed any lock file. They are fast and catch the most common class of finding before CI does.

PHP

composer audit --locked --no-interaction
  • Checks composer.lock against the PHP Security Advisories database.
  • A clean run prints nothing and exits 0.
  • Any output names a package, a CVE or advisory ID, and a severity. Resolve by upgrading the affected package or, if the path is unreachable, by adding a suppression (see below).

npm (production dependencies only)

npm audit --omit=dev --audit-level=high
cd react-frontend && npm audit --omit=dev --audit-level=high
  • --omit=dev restricts the check to packages that ship in the production bundle. Build tools, dev servers, and test frameworks are excluded; their advisories are real noise against the production risk surface.
  • --audit-level=high exits non-zero only on high or critical findings.
  • The output groups findings by severity and names the vulnerable package, the advisory, the fix version (if one exists), and the dependency path.

To see all severities (informational):

npm audit --omit=dev

Quick local Trivy scan (optional)

trivy fs . --severity CRITICAL,HIGH --ignorefile .trivyignore

Requires Trivy installed locally (brew install trivy / apt install trivy / trivy.dev/latest/getting-started/installation). This runs the same filesystem scan as CI.


Reading Scanner Output

composer audit / npm audit

Both tools print a table grouped by severity. The key fields to check:

  • Package name and version — confirm you have the affected version.
  • Advisory or CVE ID — search the advisory for the vulnerable code path. Many advisories affect features (e.g. XML parsing, OAuth callbacks) that the project may not exercise.
  • Fixed in — upgrade to at least this version. Check for lock-file conflicts before upgrading.

OWASP Dependency-Check (HTML report)

The HTML artifact produced by CI groups findings by dependency and by CVE. Columns to read:

  • Severity / CVSS — the CI gate blocks at CVSS ≥ 7 (HIGH). Lower scores are informational.
  • Evidence — shows why OWASP linked this CVE to this package. CPE mismatches (the CVE is for a package with a similar name but a different ecosystem) are the most common false-positive class.
  • Related Dependencies — shows which file in the project pulled in the affected package. If the file is a dev tool only, the production exposure is zero.

Trivy

Trivy prints a table of findings per file/layer. The table shows the library, the installed version, the fixed version, and the CVE ID. A (unfixed) note on the fixed version means no patch exists yet — container scans run with --ignore-unfixed for this reason.


Suppression Policy

Suppressions exist to keep the signal-to-noise ratio high. A suppression that hides a real finding is worse than no suppression.

When a suppression is appropriate

  • The vulnerable code path is not reachable from the project (wrong CPE match, unused feature, dev-only package).
  • No fix is available yet and the exposure is accepted pending an upgrade.
  • The finding is a kernel-level OS CVE in a container that does not run as root and has no privilege-escalation path.

A suppression is not appropriate just because a finding is inconvenient or because upgrading is difficult. If upgrading is hard, document why in the suppression and set a short review date.

What every suppression must contain

  1. CVE or advisory ID — the specific identifier being suppressed.
  2. Reason — a plain-English sentence explaining why the finding is not a real risk in this project.
  3. Review date — when the suppression should be re-evaluated (recommended: 90 days, or when the dependency next has a release).

Trivy suppression format (.trivyignore)

Each entry is a CVE ID on its own line. Inline comments (#) carry the required reason and review date:

# CVE-2099-12345: affects the XML parser feature of libfoo; project does not
# use XML parsing anywhere in the dependency graph. Review: 2026-09-23.
CVE-2099-12345

Group related entries under a shared comment block when multiple CVEs share the same reason.

OWASP Dependency-Check suppression format (owasp-suppressions.xml)

<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
  <suppress until="2026-09-23Z">
    <notes>
      CVE-2099-12345: affects the XML parser feature of example-lib; project
      does not use XML parsing. Suppressed until next release of example-lib.
    </notes>
    <packageUrl regex="true">^pkg:npm/example-lib@.*$</packageUrl>
    <cve>CVE-2099-12345</cve>
  </suppress>
</suppressions>

The until date enforces expiry — OWASP Dependency-Check will re-surface the finding after that date even if the suppression file is not updated.

Suppression hygiene rules

  • Do not include secrets, private contacts, live IP addresses, or credential paths in suppression files.
  • Explain why the finding is not reachable, not just that it has been reviewed.
  • Review suppressions when a dependency moves from development tooling into production runtime.
  • Prefer upgrading security-sensitive packages even when exposure is low.
  • When a suppression expires or a fixed version ships, remove the suppression and upgrade.

CI Scan Schedule and Gate Summary

When What runs Blocks merge?
Every push to main Full security scan + container scan Yes (composer, Trivy, TruffleHog, OWASP, npm audit)
Nightly (02:00 UTC) Full security scan Yes
PR touching package files Dependency review (GitHub Dependency Graph) Informational only (Dependency Graph not yet enabled)

The full scan does not run on PRs by default. PR-time coverage comes from the dependency-review workflow and from the fact that every merge to main triggers the full scan.


npm run check:docs
npm run check:version

Run the workflow-defined security checks in CI for release decisions. Do not run repeated local audits merely to recreate the same known noise classes.