Skip to content

svelte-security

The web-UI sibling of go-security / rust-security: one job per scanner, each opt-out-able, tool images/versions pinned per input. Three layers of coverage, all free / OSS:

Layer Jobs
Known vulns osv-scanner (OSV-DB, incl. the MAL- malicious-packages feed), npm-audit, retire
SAST / XSS semgrep (p/javascript, p/typescript, p/xss, p/secrets)
Provenance / integrity audit-signatures (npm registry signatures), lockfile-lint (every resolved URL is HTTPS + the official registry — a dependency-confusion guard)
SBOM sbom (cyclonedx-npm, informational, non-gating)
Secrets gitleaks (repo-wide, not path-filtered)

retire scans the built bundle, not node_modules — a raw node_modules scan flags vulnerable libraries vendored inside dev tooling (e.g. svelte-check bundling an old lodash) that never reach users, which would fail essentially every real project. The job builds the bundle first, then scans output_dir. Declared deps stay covered by npm-audit + osv-scanner (lockfile-based).

The malicious-package execution vector is neutralised by the npm_config_ignore_scripts job variable, set by every svelte-* component that installs — so a hostile postinstall in a transitive dependency cannot execute during build, test or lint either.

That wording used to describe an --ignore-scripts flag on the two installs in this component. It read as a pipeline-wide guarantee and was not: six of the eight installs across the track never opted in, and the unprotected ones run earlier and far more often. Protection applied per-invocation requires every new job to remember; a job variable is inherited by any install added later. See spec 0069.

A consumer whose dependency genuinely needs its install hook — a native module that compiles on install — sets ignore_install_scripts: "false" on the affected component. A project's own prebuild/postbuild hooks are unaffected: the components re-enable scripts explicitly for the commands that run first-party build scripts.

Pair with Renovate minimumReleaseAge in the consumer for the novel-malicious-release window.

retire's database

retire ships no vulnerability data. It fetches jsrepository-v5.json from raw.githubusercontent.comanonymously, on every run — and fails the job outright when GitHub rate-limits it:

Error downloading: ...jsrepository-v5.json: HTTP 429 Too Many Requests

That limit is per source IP, and CI shares one. It fails closed on an unrelated axis: a GitHub limit blocks any merge request touching the svelte track, whatever the change was about.

The database is now resolved in three tiers, each logged:

tier condition behaviour
1 retire_jsrepo_cache file exists --jsrepo <cache>no network call at all
2 github_token set authenticated fetch, then --jsrepo
3 neither retire downloads anonymously — today's behaviour, may 429

Tier 3 is a deliberate fallback: a component that hard-failed without a token would break consumers who have not set one, to fix a problem they may not have.

Two things worth knowing if you touch this:

  • --jsrepo, not --cachedir. Both exist. --cachedir relocates retire's own cache but leaves it deciding whether to re-fetch; --jsrepo takes the database as given and makes no call.
  • The fetcher is detected, not assumed. npm_image defaults to node-tools, which has curl and no wget; a consumer overriding it with a node:*-alpine gets wget and no curl. The job uses whichever is present. Hardcoding one shipped red once — worth remembering that this job only runs on merge requests touching this component, so a runtime assumption fails rarely and inconveniently.
  • A local database still detects. Verified against a known-vulnerable fixture: exit 13, jquery 1.4.2 has known vulnerabilities … CVE-2011-4969 … CVE-2019-11358. The check is intact, not merely quiet — which is the failure mode to watch for in any change here.

See spec 0071.

Always-on — unlike the other svelte-* components, this carries no change-detection changes input; every MR re-checks deps against the latest advisory DB and re-scans for secrets. See Explanation: security, always-on. paths is required (no default).

Jobs

Job What it runs
semgrep semgrep --error --severity $[[ inputs.semgrep_severity ]] $[[ inputs.semgrep_config ]] $[[ inputs.paths ]]
osv-scanner Per path: /osv-scanner -L "$dir/package-lock.json"
npm-audit Per path: npm audit --audit-level=$[[ inputs.audit_level ]]
retire Per path: npm ci --ignore-scripts && npm run $[[ inputs.build_script ]] && npx retire@$[[ inputs.retire_version ]] --path "$[[ inputs.output_dir ]]"
audit-signatures Per path: npm ci --ignore-scripts && npm audit signatures
lockfile-lint Per path: npx lockfile-lint@$[[ inputs.lockfile_lint_version ]] --path package-lock.json --type npm --validate-https --allowed-hosts $[[ inputs.lockfile_lint_allowed_hosts ]]
sbom Per path: npx @cyclonedx/cyclonedx-npm@$[[ inputs.cyclonedx_version ]] --package-lock-only, artifacts under sbom/
gitleaks gitleaks detect, scoped to the MR diff base

Inputs

Input Type Default Description
stage string security GitLab CI stage.
if string '$CI_PIPELINE_SOURCE == "merge_request_event"' Gating rules:if:. No change-detection — runs on every MR.
ignore_install_scripts string "true" Blocks npm lifecycle scripts during dependency installation, via the npm_config_ignore_scripts job variable so every install in the job inherits it. Set "false" only if a dependency needs its install hook (a native module that compiles on install); the project's own prebuild/postbuild are unaffected. See spec 0069.
paths string (required) Space-separated frontend root(s).
npm_image string node-tools:v0.1.0 Image for the npm-based jobs.
semgrep_image string "returntocorp/semgrep:1.172.0" Semgrep image (shared pin with go-security).
semgrep_config string "--config p/javascript --config p/typescript --config p/xss --config p/secrets" Semgrep ruleset flags.
semgrep_severity string "ERROR" Minimum severity semgrep fails on.
osv_scanner_image string "ghcr.io/google/osv-scanner:v2.4.0" OSV-Scanner image.
osv_cache_dir string "/opt/ci-cache/osv" Shared osv-scanner offline-DB cache. When the npm OSV DB exists here, osv-scanner runs offline for each root instead of calling api.osv.dev live. Absent → online exactly as before. See the shared-cache spec.
retire_jsrepo_cache string "/opt/ci-cache/retire/jsrepository-v5.json" Shared retire.js database file. Present → retire runs against it with --jsrepo and makes no network call. Absent → falls through to an authenticated fetch. See retire's database.
github_token string "$GITHUB_COM_TOKEN" Token for fetching retire's database from the GitHub contents API when no cache is present. Empty is tolerated and degrades to retire's own anonymous download.
gitleaks_image string "ghcr.io/gitleaks/gitleaks:v8.30.1" Gitleaks image (shared pin with go-security).
audit_level string "high" npm audit --audit-level= threshold.
lockfile_lint_allowed_hosts string "npm" lockfile-lint --allowed-hosts registry alias.
build_script string "build" npm script producing the shipped bundle, built before retire scans it.
output_dir string "embed" Built-bundle directory retire scans.
retire_version string "5.4.3" retire.js version (Renovate-bumped).
lockfile_lint_version string "5.0.0" lockfile-lint version.
cyclonedx_version string "5.0.0" @cyclonedx/cyclonedx-npm version.

Usage

include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
    inputs:
      paths: "pkg/studio/web"

See also