Summary
Add the remaining in-repo supply-chain hardening identified in the security posture audit: a dependency-review gate on PRs, invisible/homoglyph-character detection in CI (+ light ESLint), and least-privilege permissions: defaults across workflows. Complements #781 (publish provenance) — this issue does not touch publish workflows.
Background
A posture audit confirmed the foundational controls are in place (branch protection, push protection, secret scanning, Renovate, --frozen-lockfile everywhere, all third-party actions pinned by SHA, pnpm onlyBuiltDependencies allowlist). Three lower-severity in-repo gaps remain:
- No
dependency-review-action — PRs that bump to a known-vulnerable or typosquatted package are not blocked before merge (Renovate opens the PR, but nothing validates the diff's dependency changes).
- No invisible-character / homoglyph detection — zero-width (U+200B–200D), bidi-override (U+202A–202E, "Trojan Source"), and BOM (U+FEFF) characters are invisible in GitHub's diff UI and in most editors. They compile fine, which is exactly the risk (identifier-splitting / string-literal injection). Neither ESLint nor CI currently flags them.
- 6 workflows lack a top-level
permissions: block (cli-release, code-qa, codeql, e2e, marketplace-publish, release-validation) — these inherit the broad default GITHUB_TOKEN, violating least privilege.
Changes required
1. Dependency review on PRs (code-qa.yml)
Add a new job:
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/dependency-review-action@c74b580d73376b7750d3d2a50bfb8adc2c937507 # v4
2. Invisible / homoglyph character detection
Two layers, with deliberate division of labor:
- ESLint
no-irregular-whitespace (error, skipStrings: true) in the per-workspace eslint.config.mjs files — fast local feedback in the pre-commit hook; catches identifier/token splitting. skipStrings: true keeps it quiet in the non-English i18n locale files. The string-literal variant is intentionally left to the CI grep (below), which is the stronger control.
.vscode/settings.json:
{ "editor.unicodeHighlight.invisibleCharacters": true,
"editor.unicodeHighlight.ambiguousCharacters": true }
- CI grep (authoritative defense, incl. whitespace in strings) — scans raw file bytes, so it catches zero-width chars in strings, identifiers, comments, and between tokens, plus the full Trojan-Source bidi set (U+202A–202E) and soft hyphens (U+00AD) that ESLint skips. Add a job to
code-qa.yml:
invisible-chars:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Reject invisible / homoglyph Unicode
run: |
! grep -rnP '[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}\x{00AD}]' \
--include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' \
src webview-ui packages apps
(May need a small allowlist if any locale/test file legitimately contains these chars.)
Rationale: ESLint's skipStrings: false would only scan AST string literals and miss template/dynamic strings + .json/.md. The raw-byte grep gives broader coverage with less i18n-noise risk, so it's the belt-and-suspenders the string case actually needs.
3. Least-privilege permissions: defaults
Add permissions: contents: read (escalating only where needed) to the 6 workflows that currently lack a top-level block: cli-release.yml, code-qa.yml, codeql.yml, e2e.yml, marketplace-publish.yml, release-validation.yml. Leave the publish jobs' existing job-level permissions intact.
Out of scope
Acceptance criteria
Summary
Add the remaining in-repo supply-chain hardening identified in the security posture audit: a dependency-review gate on PRs, invisible/homoglyph-character detection in CI (+ light ESLint), and least-privilege
permissions:defaults across workflows. Complements #781 (publish provenance) — this issue does not touch publish workflows.Background
A posture audit confirmed the foundational controls are in place (branch protection, push protection, secret scanning, Renovate,
--frozen-lockfileeverywhere, all third-party actions pinned by SHA, pnpmonlyBuiltDependenciesallowlist). Three lower-severity in-repo gaps remain:dependency-review-action— PRs that bump to a known-vulnerable or typosquatted package are not blocked before merge (Renovate opens the PR, but nothing validates the diff's dependency changes).permissions:block (cli-release,code-qa,codeql,e2e,marketplace-publish,release-validation) — these inherit the broad defaultGITHUB_TOKEN, violating least privilege.Changes required
1. Dependency review on PRs (
code-qa.yml)Add a new job:
2. Invisible / homoglyph character detection
Two layers, with deliberate division of labor:
no-irregular-whitespace(error,skipStrings: true) in the per-workspaceeslint.config.mjsfiles — fast local feedback in the pre-commit hook; catches identifier/token splitting.skipStrings: truekeeps it quiet in the non-English i18n locale files. The string-literal variant is intentionally left to the CI grep (below), which is the stronger control..vscode/settings.json:{ "editor.unicodeHighlight.invisibleCharacters": true, "editor.unicodeHighlight.ambiguousCharacters": true }code-qa.yml:3. Least-privilege
permissions:defaultsAdd
permissions: contents: read(escalating only where needed) to the 6 workflows that currently lack a top-level block:cli-release.yml,code-qa.yml,codeql.yml,e2e.yml,marketplace-publish.yml,release-validation.yml. Leave the publish jobs' existing job-level permissions intact.Out of scope
Acceptance criteria
dependency-reviewjob runs on PRs and fails on a known-vulnerable/typosquatted bumpno-irregular-whitespaceenabled (skipStrings: true);pnpm lintis clean.tsfile (incl. inside a string literal)permissions:block; CI still green