diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d485f7f..f252057 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,3 +19,5 @@ drove it on. - [ ] Edited `lib/shell/` rather than a stamped region, ran `node plugins/vstack/lib/build-shell.mjs stamp`, and committed both. - [ ] Added or renamed a plugin, and updated `.claude-plugin/marketplace.json` in the same commit. - [ ] Renamed a tool, and added its former directory name to the `LEGACY` map in `lib/workdir.mjs`. +- [ ] Every security scan passes, and no finding was silenced instead of fixed. +- [ ] Added a step that uses an action, and pinned it by commit SHA with the version in a trailing comment. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..56a9f92 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 + +# Actions are pinned by commit SHA, which is immutable and therefore never +# picks up an upstream fix on its own. Dependabot moves the pin and rewrites +# the version comment beside it. +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + # A compromised release is usually pulled within days of publication, so + # wait before moving a pin onto it. + cooldown: + default-days: 7 + commit-message: + prefix: ci diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0e0e35..c84275d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,10 @@ jobs: matrix: node: ['18', '22'] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node }} - name: Review lifecycle @@ -33,8 +35,10 @@ jobs: name: Stamped shell is current runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22' # Fails when a page's stamped region has drifted from lib/shell/. @@ -46,12 +50,16 @@ jobs: name: Manifests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '22' + # Unpinned on purpose: this job has to run the validator a marketplace + # reviewer would run today, and there is no lockfile to pin it against. - name: Install Claude Code - run: npm install --global @anthropic-ai/claude-code + run: npm install --global @anthropic-ai/claude-code # zizmor: ignore[adhoc-packages] # The community-marketplace review pipeline runs this same check on every # submission, so a warning here is a warning a reviewer would see. - name: Validate the marketplace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59b6607..3317dd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,9 @@ jobs: name: Tag matches the manifest version runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # An explicit version in plugin.json is what Claude Code compares against # to decide an update exists. Tagging a release without bumping it leaves # every installed copy believing it is already current. diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..1843b35 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,39 @@ +name: Scorecard + +# OpenSSF Scorecard rates the repository's supply-chain posture: branch +# protection, pinned actions, token permissions, release signing. It reads the +# repository rather than the code, so it runs on main and on a schedule, not on +# a pull request. +on: + branch_protection_rule: + push: + branches: [main] + schedule: + - cron: '41 5 * * 1' + +permissions: + contents: read + +jobs: + analysis: + name: OpenSSF Scorecard + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + # Publishing the result is what lets the README badge read a score. + id-token: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run Scorecard + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - name: Upload the result + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + sarif_file: results.sarif diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..956080b --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,123 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + # A weekly run finds a rule or advisory published after the last commit. + - cron: '23 5 * * 1' + +permissions: + contents: read + +jobs: + sast: + name: SAST (CodeQL) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Initialise CodeQL + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + with: + # HTML is scanned too: every page carries inline script. + languages: javascript-typescript + queries: security-and-quality + - name: Analyse + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + + secrets: + name: Secret scan (Gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Anything ever committed is compromised, so the scan reads the whole + # history. A shallow clone shows only the tip. + fetch-depth: 0 + persist-credentials: false + - name: Install Gitleaks + env: + # The GitHub Action wrapper needs a paid licence for organisation + # repositories. The CLI it wraps is MIT, so this installs that. + GITLEAKS_VERSION: 8.30.1 + run: | + set -euo pipefail + cd "$RUNNER_TEMP" + release="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" + curl -fsSL -O "${release}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + curl -fsSL -O "${release}/gitleaks_${GITLEAKS_VERSION}_checksums.txt" + grep "gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + "gitleaks_${GITLEAKS_VERSION}_checksums.txt" | sha256sum -c - + tar -xzf "gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" gitleaks + - name: Scan every commit + run: | + "$RUNNER_TEMP/gitleaks" git --no-banner --redact --verbose + + workflows: + name: Workflow audit (zizmor) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # The workflows are the only thing here that runs with repository + # credentials, so they get their own static analysis. + - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + version: 1.29.0 + + quality: + name: Code quality (SonarQube Cloud) + runs-on: ubuntu-latest + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Sonar attributes findings to the commit that introduced them. + fetch-depth: 0 + persist-credentials: false + - name: Scan + if: env.SONAR_TOKEN != '' + uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1 + # A pull request from a fork cannot read the token, and neither can a + # clone that has not been bound to a SonarQube Cloud project yet. + - name: Report the skip + if: env.SONAR_TOKEN == '' + run: | + echo "SONAR_TOKEN is not set, so the code-quality scan did not run." \ + >> "$GITHUB_STEP_SUMMARY" + + gates: + name: Security gates + runs-on: ubuntu-latest + needs: [sast, secrets, workflows, quality] + if: always() + steps: + # `needs` alone does not gate: a skipped or cancelled scan satisfies it. + # This fails unless every scan actually reported success. + - name: Every scan succeeded + env: + RESULTS: ${{ toJSON(needs) }} + run: | + set -euo pipefail + failed=$(echo "$RESULTS" | jq -r ' + to_entries[] | select(.value.result != "success") | "\(.key): \(.value.result)" + ') + if [ -n "$failed" ]; then + echo "These scans did not succeed:" + echo "$failed" + exit 1 + fi + echo "Every scan succeeded." diff --git a/CLAUDE.md b/CLAUDE.md index 6166d01..40263ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,20 @@ claude plugin validate ./plugins/vstack --strict # the plugin manifest `.github/workflows/ci.yml` runs all of the above on every pull request. +The security scans run in `.github/workflows/security.yml`, and a merge is +blocked until every one of them passes. Two of them run locally: + +```bash +gitleaks git --no-banner --redact --verbose # secrets, over the full history +uvx zizmor@1.29.0 .github/workflows/ # workflow audit +``` + +`SECURITY.md` owns what each gate enforces, the rule that a finding is fixed +rather than silenced, and the rule that every action is pinned by commit SHA +with its version in a trailing comment. Adding a step that uses an action means +resolving that SHA with +`gh api repos///commits/ --jq .sha`. + CI cannot install the plugin, so rehearse that locally before a release. `CLAUDE_CONFIG_DIR` keeps it out of the real config: without it, a local-path marketplace is written to user settings and shadows the published diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de78968..a22fdf1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,23 @@ claude plugin validate ./plugins/vstack --strict Nothing runs end to end in CI, so a green build is not a tested skill. +## Passing the security gates + +CI also runs the scans listed in [SECURITY.md](SECURITY.md), and a merge is blocked until all of them pass. Two of them you can run before you push: + +```bash +gitleaks git --no-banner --redact --verbose # every commit, not the working tree +uvx zizmor@1.29.0 .github/workflows/ # only if you touched a workflow +``` + +CodeQL and SonarQube report in the pull request itself. Read the finding before assuming it is noise — the servers read from disk and the pages build DOM from stored comments, which is exactly where a real one would appear. + +When you add a step that uses an action, pin it by commit SHA and put the version in a trailing comment, the way the existing steps do. Copy the SHA from the release you intend to use: + +```bash +gh api repos///commits/ --jq .sha +``` + CI also cannot install the plugin. Rehearse that locally before a release, with `CLAUDE_CONFIG_DIR` pointed at a throwaway directory so the local-path marketplace is not written to your real settings, where it would shadow the published `cavalry-collective`: ```bash diff --git a/README.md b/README.md index d972f94..248d44d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ # Visual Stack +[![CI](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/ci.yml/badge.svg)](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/ci.yml) +[![Security](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml/badge.svg)](https://github.com/Cavalry-Collective/visual-stack/actions/workflows/security.yml) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Cavalry-Collective/visual-stack/badge)](https://scorecard.dev/viewer/?uri=github.com/Cavalry-Collective/visual-stack) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + ## Stop prompting. Start pointing. Visual Stack adds a Figma-like feedback layer to AI coding agents. @@ -93,6 +98,10 @@ Each workspace is linked to one agent session. The link holds while that session ![Your comments are submitted as one review round. The agent claims the round and reads its brief, asking for clarification when a comment is unclear. Comments sent while the round is in progress join it. Publishing is blocked until every comment has been applied, answered, or dismissed, and the published version appears in the same workspace.](docs/assets/review-lifecycle.svg) +## Security + +Installing this plugin runs its code on your machine, so every change to `main` passes a set of scans before it lands: static analysis, a secret scan over the whole history, a workflow audit, and a code-quality gate. The badges above report the last run. [SECURITY.md](SECURITY.md) says what each gate enforces and how to report a vulnerability. + ## Contribute Visual Stack is open source and under active development. Expect rough edges, breaking changes, and occasional moments of character development. diff --git a/SECURITY.md b/SECURITY.md index 035f74a..d90aa2f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,6 +19,24 @@ In scope: Out of scope: vulnerabilities in Claude Code itself — report those to Anthropic. +## What runs on every change + +`.github/workflows/security.yml` runs these scans on every pull request, on every push to `main`, and weekly. The `Security gates` job fails unless all four report success, so a scan that is skipped or cancelled blocks the merge in the same way a failing one does. + +| Scan | Tool | What it enforces | +|---|---|---| +| Static analysis | CodeQL, `security-and-quality` suite | Injection, path traversal, and unsafe DOM construction in the servers and the pages. Findings land in the repository's Security tab. | +| Secret scan | Gitleaks | No credential in any commit. It reads the full history, not the working tree, because anything ever committed is compromised. | +| Workflow audit | zizmor | The workflows themselves: token permissions, credential persistence, and untrusted input reaching a `run` block. | +| Code quality | SonarQube Cloud | Bugs, security hotspots, and maintainability on new code. Free for public projects. The scan reports a skip rather than failing where `SONAR_TOKEN` is unavailable, which is every pull request from a fork. | + +`.github/workflows/scorecard.yml` runs OpenSSF Scorecard weekly and on `main`. It rates the repository rather than the code — branch protection, pinned actions, token permissions — and publishes the score the README badge reads. + +Two rules keep those gates meaningful: + +- Fix a finding rather than silencing it. A suppression carries a comment saying why it cannot be fixed, next to the line it applies to. +- Actions are pinned by commit SHA with the version in a trailing comment. A tag moves, and a moved tag runs code nobody reviewed. Dependabot proposes the bumps weekly. + ## Supported versions `main` only. There are no maintained release branches. diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..3b895ab --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,13 @@ +# Read by the SonarQube Cloud scan in .github/workflows/security.yml. +# The project key and organisation are the ones SonarQube Cloud derives when a +# GitHub repository is imported. +sonar.projectKey=Cavalry-Collective_visual-stack +sonar.organization=cavalry-collective +sonar.projectName=Visual Stack + +sonar.sources=. +sonar.exclusions=docs/assets/**,node_modules/**,**/.vstack/** + +# The shared shell is stamped into every page by lib/build-shell.mjs, because a +# page has to work with no external requests. That duplication is the design. +sonar.cpd.exclusions=plugins/vstack/**/*.html