Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ this directory).
escalations, serialized lanes, gates and close-out. The main session
decides and raises gates; landers do the reviewing, merging,
verifying and releasing.
- `owasp-scan` — a parallel OWASP Top Ten scan of `src/`.
- `owasp-scan` — a parallel OWASP Top 10:2025 scan of a whole repo
(all languages, workflows, Dockerfiles, compose, scripts, config) plus
the permanent scanners, with ASVS 5.0 references and a report file per
run; takes an optional `asvs=V3,V7,V10` manual-review focus. Its
target discovery is `skills/owasp-scan/discover-targets.sh` (run with
`bash`, read-only; `--summary` and `--skipped` show what it skips).

Base lists each of these skills one by one in its sync manifest, so an
instance adds its own as `skills/<name>/` and lists that directory
Expand Down
441 changes: 368 additions & 73 deletions .claude/skills/owasp-scan/SKILL.md

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions .claude/skills/owasp-scan/discover-targets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# Print the owasp-scan target list for the repository in the current
# directory, one "<category><TAB><path>" line per file (read-only).
#
# Every git-tracked file is a target unless it is skipped for one of
# these reasons (tracked files are source, so there are no test, build,
# dist, target or generated exclusions):
# lockfile an explicit list of lockfile names (osv-scanner's job)
# vendored vendor/, third_party/, node_modules/ at any depth
# binary / empty not text
# too-large over 256 KiB (listed by --skipped, reported as not scanned)
# link-or-submodule a symlink or gitlink: its content is not scanned here
# missing tracked but absent from the worktree
#
# Category: the first rule that matches wins, in this order:
# 1 workflow .github/workflows/*.yml|*.yaml
# 2 workflow composite action (action.yml|action.yaml at any depth)
# 3 docker Dockerfile, Dockerfile.*, *.Dockerfile, Containerfile[.*]
# 4 compose compose[.*].y[a]ml, docker-compose*.y[a]ml
# 5 code by extension:
# shell *.sh *.bash *.zsh Makefile GNUmakefile *.mk
# python *.py tsjs *.ts *.tsx *.js *.jsx *.mjs *.cjs *.mts *.cts
# rust *.rs
# 6 shebang interpreter basename (also via /usr/bin/env [-S] [VAR=x] X):
# sh bash dash zsh ksh ash -> shell; python* pypy* -> python;
# node deno bun tsx ts-node -> tsjs
# 7 config *.yml *.yaml *.json *.jsonc *.toml *.ini *.cfg *.conf
# *.properties *.env .env .env.* Caddyfile
# 8 generic any other text file
#
# Usage: discover-targets.sh [--summary] [--skipped]
# (default) targets on stdout as "<category><TAB><path>", sorted
# --summary target counts per category, then "not-scanned <reason> <n>"
# --skipped "skip:<reason><TAB><path>" for every skipped tracked file
set -euo pipefail

mode=targets
for arg in "$@"; do
case "$arg" in
--summary|--skipped)
if [ "$mode" != targets ] && [ "$mode" != "${arg#--}" ]; then echo "--summary and --skipped cannot be combined" >&2; exit 2; fi
mode=${arg#--} ;;
*) echo "unknown argument: $arg" >&2; exit 2 ;;
esac
done

MAX_BYTES=262144 # files over 256 KiB are listed, not scanned

skip_reason() { # $1 = path; prints a reason when the file is never a target
case "${1##*/}" in
package-lock.json|npm-shrinkwrap.json|pnpm-lock.yaml|yarn.lock|bun.lock|bun.lockb|deno.lock|flake.lock|composer.lock|Cargo.lock|uv.lock|poetry.lock|pdm.lock|Pipfile.lock|Gemfile.lock|go.sum|gradle.lockfile|packages.lock.json|Podfile.lock|mix.lock|pubspec.lock|.terraform.lock.hcl) echo lockfile; return ;;
esac
case "$1" in
node_modules/*|*/node_modules/*|vendor/*|*/vendor/*|third_party/*|*/third_party/*) echo vendored ;;
esac
}

SHEBANG_KIND=""
shebang_kind() { # $1 = path; sets SHEBANG_KIND from the interpreter basename
local first="" words word interp
SHEBANG_KIND=""
IFS= read -r first < "$1" 2>/dev/null || true
first=${first%$'\r'}
case "$first" in '#!'*) ;; *) return 0 ;; esac
read -r -a words <<< "${first#\#!}"
[ "${#words[@]}" -gt 0 ] || return 0
interp=${words[0]##*/}
if [ "$interp" = env ]; then
interp=""
for word in "${words[@]:1}"; do
case "$word" in -*|*=*) continue ;; esac
interp=${word##*/}
break
done
fi
case "$interp" in
sh|bash|dash|zsh|ksh|ash) SHEBANG_KIND=shell ;;
python*|pypy*) SHEBANG_KIND=python ;;
node|deno|bun|tsx|ts-node) SHEBANG_KIND=tsjs ;;
esac
}

categorise() { # $1 = path; prints the category
local base=${1##*/}
case "$1" in
.github/workflows/*.yml|.github/workflows/*.yaml) echo workflow; return ;;
esac
case "$base" in
action.yml|action.yaml) echo workflow; return ;;
Dockerfile|Dockerfile.*|*.Dockerfile|Containerfile|Containerfile.*) echo docker; return ;;
compose.yml|compose.yaml|compose.*.yml|compose.*.yaml|docker-compose*.yml|docker-compose*.yaml) echo compose; return ;;
*.sh|*.bash|*.zsh|Makefile|GNUmakefile|*.mk) echo shell; return ;;
*.py) echo python; return ;;
*.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.mts|*.cts) echo tsjs; return ;;
*.rs) echo rust; return ;;
esac
shebang_kind "$1"
if [ -n "$SHEBANG_KIND" ]; then echo "$SHEBANG_KIND"; return; fi
case "$base" in
*.yml|*.yaml|*.json|*.jsonc|*.toml|*.ini|*.cfg|*.conf|*.properties|*.env|.env|.env.*|Caddyfile) echo config ;;
*) echo generic ;;
esac
}

emit() { # prints "<category-or-skip:reason>\t<path>" for every tracked file
local f reason
while IFS= read -r -d '' f; do
if [ -L "$f" ] || [ -d "$f" ]; then printf 'skip:link-or-submodule\t%s\n' "$f"; continue; fi
if [ ! -e "$f" ]; then printf 'skip:missing\t%s\n' "$f"; continue; fi
reason=$(skip_reason "$f")
if [ -n "$reason" ]; then printf 'skip:%s\t%s\n' "$reason" "$f"; continue; fi
if [ ! -s "$f" ]; then printf 'skip:empty\t%s\n' "$f"; continue; fi
if ! grep -Iq -- '' < "$f" 2>/dev/null; then printf 'skip:binary\t%s\n' "$f"; continue; fi
if [ "$(wc -c < "$f")" -gt "$MAX_BYTES" ]; then printf 'skip:too-large\t%s\n' "$f"; continue; fi
printf '%s\t%s\n' "$(categorise "$f")" "$f"
done < <(git ls-files -z)
}

case "$mode" in
targets) emit | grep -v '^skip:' | sort || true ;;
skipped) emit | grep '^skip:' | sort || true ;;
summary)
all=$(emit)
if [ -n "$all" ]; then
printf '%s\n' "$all" | grep -v '^skip:' | cut -f1 | sort | uniq -c | sort -rn || true
printf '%s\n' "$all" | grep '^skip:' | cut -f1 | sort | uniq -c | sed -E 's/^ *([0-9]+) skip:(.*)$/not-scanned \2 \1/' || true
fi
;;
esac
12 changes: 8 additions & 4 deletions .github/CONTENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
- `workflows/` — GitHub Actions; see its own `README.md`.
- `scripts/` — helper scripts used by the workflows; see its own
`README.md`.
- `renovate.json` — weekly update PRs for GitHub Actions, every
- `renovate.json` — weekly update PRs for GitHub Actions (pinned to a
full commit SHA with a `# vX.Y.Z` comment, kept current by
`helpers:pinGitHubActionDigests`), pre-commit hook revisions (the
`pre-commit` manager), Docker image digests, GitHub and OSV
vulnerability alerts, every
Dockerfile/compose image tag repo-wide (scanned recursively, unlike
Dependabot), and the `# renovate:` comment-annotated version pins in
`.devcontainer/Dockerfile` (and an instance's top-level runtime
Expand All @@ -30,6 +34,6 @@

## Don't

- Pin an Action to a floating tag like `@v7` or `@main` — pin the exact
patch version (see `docs/TEMPLATE.md`'s "Versions and config"
section).
- Pin an Action to a floating tag like `@v7` or `@main`, or to a tag at
all — pin the full commit SHA with a `# vX.Y.Z` comment (see
`docs/TEMPLATE.md`'s "Versions and config" and "Checks" sections).
14 changes: 13 additions & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
"schedule": ["before 6am on monday"],
"pre-commit": {
"enabled": true
},
"vulnerabilityAlerts": {
"labels": ["security"]
},
"osvVulnerabilityAlerts": true,
"packageRules": [
{
"matchUpdateTypes": ["patch", "minor"],
Expand All @@ -20,6 +27,11 @@
"groupName": "python minor upgrade",
"automerge": false
},
{
"description": "Pin container images to their digest. Scoped to the docker managers on purpose: a global pinDigests also hits the pre-commit manager, whose tag revs can't be converted to digests (the update fails). GitHub Actions are pinned by helpers:pinGitHubActionDigests instead.",
"matchManagers": ["dockerfile", "docker-compose"],
"pinDigests": true
},
{
"description": "Renovate can't regenerate .devcontainer/devcontainer-lock.json, so features are bumped by hand together with it (see .devcontainer/README.md).",
"matchManagers": ["devcontainer"],
Expand Down
14 changes: 4 additions & 10 deletions .github/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@
that label the test fails until you look up the new image's `python3`
(actions/runner-images) and update `RUNNER_IMAGE_PYTHON`, `RUNNER_PYTHON` and
`ruff.toml` together.
- `test_moderate_labels.py` — checks that every `create "<name>" "<color>"
"<description>"` line in `../workflows/moderate-setup.yml` has a description
of at most 100 characters (GitHub answers HTTP 422 to a longer one, and only
the manually dispatched workflow would ever show it); `scripts-tests` runs it.
- `verify_release_tag.sh` — the `release` job's check that an already
published release's tag points at the commit the run built (fails closed;
retries only transient lookup errors, six attempts over about two
Expand All @@ -98,16 +102,6 @@
read-after-write lag for refs, so the error says to re-run once the tag is
visible. Tested against a fake `gh` in
`test_release_scripts.py`.
- `find_moderation_result.sh` / `test_moderation_scripts.py` — the check
step shared by `../workflows/moderate-bug-fix-apply.yml` and
`../workflows/moderate-feature-build-apply.yml`: asks the API for the
`moderation-result` artifact by name (so it is found however many
artifacts the run has) and writes `found=true|false`. It fails on an
expired artifact (a produced result that can no longer be applied; the
message says to re-run the worker run, since re-running the apply run
cannot bring the artifact back), an
API error, and an empty or unrecognised response. The tests run it
against a fake `gh`.
- `run_claude.sh` — invokes `claude -p` for one issue-moderation stage
inside the devcontainer; shared by all four Claude-invoking
`../workflows/moderate-*.yml` jobs. See `../workflows/README.md`'s
Expand Down
40 changes: 0 additions & 40 deletions .github/scripts/find_moderation_result.sh

This file was deleted.

Loading