Skip to content

chore(security): dependency-review, invisible-char detection, and least-privilege workflow permissions #782

Description

@edelauna

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

  • dependency-review job runs on PRs and fails on a known-vulnerable/typosquatted bump
  • no-irregular-whitespace enabled (skipStrings: true); pnpm lint is clean
  • CI invisible-char grep fails when a zero-width char is planted in a .ts file (incl. inside a string literal)
  • All 6 workflows have an explicit top-level permissions: block; CI still green

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions