Prompt used to generate scan-vulnerabilities-in-react-apps.sh
Date: 2026-08-21
Author: Roman

--------------------------------------------------------------------------------
ORIGINAL REQUEST
--------------------------------------------------------------------------------

I have the dev.react server for our react apps, and I would like to have some
script which will scan the apps we have on this server and find me the apps which
have vulnerabilities. We already have a similar script which scans the ports used
by react apps:

  react-env/cronjob-scripts/scan-ports-used-by-react-apps/scan-ports-used-by-react-apps.sh

Place the new script in react-env/cronjob-scripts/ and email the results to
roman@boldporch.com.

--------------------------------------------------------------------------------
DECISIONS MADE DURING PLANNING
--------------------------------------------------------------------------------

Scope        : npm dependency CVEs only (npm audit). Not OS packages, not config
               auditing, not outdated-framework checks.
Cadence      : email ONLY on delta - a new finding, or an existing one escalating
               to a higher severity. Plus a weekly heartbeat digest so a broken
               cron job is distinguishable from a quiet week.
Hosts        : dev.react and prod.react, cron on each box (not SSH from a laptop).
Credentials  : gitignored vuln-scan-config.ini next to the script, matching the
               confluence-credentials.ini convention already used in this repo.

--------------------------------------------------------------------------------
RESEARCH FINDINGS THAT SHAPED THE DESIGN
--------------------------------------------------------------------------------

Server layout
  /home/{user}/domains/{domain}/app/            <- live code (Deployer pins
                                                   release_path to this)
  /home/{user}/domains/{domain}/deployments/    <- releases/, current/, shared/
  ~50 Node apps on dev.react across u-staging, u-roman, u-kyle.
  package.json, package-lock.json AND a full node_modules all live on the box -
  deploys run `npm install` + `npm run build` on the target host.

Existing tooling gap
  No Dependabot, no CodeQL/Snyk/Trivy/OSV anywhere in the ecosystem.
  knowledge/engineering/security-baseline.md is a stub ("CVE policy: TODO").
  operations/runbooks/dependency-update.md is an empty scaffold.
  Only manual `npm audit fix` sweeps a few times a year.

Email
  No email-sending code existed anywhere in server-admin-tools - this is the first.
  No local MTA on the React hosts, and Vultr blocks outbound port 25, so
  mail/mailx/sendmail are not options.
  mail.boldporch.com (10.2.112.19) is the ecosystem's only transactional relay,
  on the same private /24 as dev.react (10.2.112.16) and prod.react (10.2.112.17).
  Ports 465 (implicit TLS) and 587 (STARTTLS) are both live.
  => curl talking SMTPS directly. Nothing to install.

Rejected alternatives
  - sender-microservice / nodejs-notification-microservice: both are RabbitMQ
    workers with no HTTP surface, and both hard-rewrite the recipient to
    any-notification-test@boldporch.com outside prod.
  - bp-admin POST /email/send: real and unauthenticated, but its DTO restricts
    `template` to three analytics templates via @IsEnum + a whitelisting
    ValidationPipe, so arbitrary body text is impossible.
  - osv-scanner: more thorough than npm audit, but needs a binary installed on
    every host. npm audit is already there and matches what the team fixes today.

--------------------------------------------------------------------------------
IMPLEMENTATION NOTES / GOTCHAS HANDLED
--------------------------------------------------------------------------------

npm audit JSON (v7+ shape, verified against a real BoldPorch lockfile):
  .metadata.vulnerabilities -> counts by severity
  .vulnerabilities[pkg]     -> { severity, isDirect, fixAvailable, via[], range }
  .via[] is EITHER a string (transitive chain reference) OR an object (the
    actual advisory, carrying title/url/severity/cvss/range).
  .fixAvailable is EITHER a boolean OR an object {name, version, isSemVerMajor}.
  Findings are emitted one row per distinct (package, advisory) pair, deduped.
  npm v6's .advisories shape is detected and reported rather than silently
  producing zero findings.

npm audit exits NON-ZERO when it finds vulnerabilities. Under `set -e` that
  looks like a crash, so the call is wrapped in `|| true`.

Discovery uses process substitution - `while ... done < <(find ...)` - NOT
  `find | while`. The port-scan script uses the pipe form, which runs the loop
  body in a subshell where the counters can never escape. This script keeps
  running totals, so it must not repeat that.

Read-only guarantee: --package-lock-only makes it impossible for npm to install
  or touch node_modules. npm_config_cache is redirected to /tmp so the run does
  not pollute /root/.npm or any user's home.

Email HTML uses inline style="..." on every element. The port-scan script's
  csv_to_html_table() emits Confluence classes (confluenceTable/confluenceTh)
  which mean nothing to an email client - it had to be rewritten, not reused.

SMTP password goes into a 0600 file passed via `curl --config`, never on the
  command line, so it cannot be read out of `ps auxww` during the send.

MIME details: mandatory blank line between headers and body; base64 attachment
  wrapped at 76 chars (GNU `-w` vs BSD `-b` detected at runtime); body lines
  consisting of a single "." are dot-stuffed so they cannot terminate DATA early.

Delta key is user|domain|package|advisory_id and deliberately EXCLUDES severity,
  so a severity change registers as an escalation rather than as a simultaneous
  new + resolved pair.

State is written only after a successful send, so a transient SMTP failure
  cannot silently swallow a newly-appeared critical CVE.

--------------------------------------------------------------------------------
KNOWN LIMITATION
--------------------------------------------------------------------------------

Deploys run `npm install`, not `npm ci`, so the installed node_modules tree can
drift from package-lock.json. The default scan_mode=lockfile audits the lockfile
(fast, deterministic, matches what a fix would change) but is not a guarantee of
what is actually running. scan_mode=installed audits the real tree instead.
