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.com — anonymously, on
every run — and fails the job outright when GitHub rate-limits it:
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.--cachedirrelocates retire's own cache but leaves it deciding whether to re-fetch;--jsrepotakes the database as given and makes no call.- The fetcher is detected, not assumed.
npm_imagedefaults tonode-tools, which has curl and no wget; a consumer overriding it with anode:*-alpinegets 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¶
svelte-build,svelte-lint,svelte-test- Explanation: security, always-on
- Spec:
0031-svelte-frontend-track