diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb49ff9..8ad938e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,7 +58,10 @@ Same as above, plus: `references/` file rather than presenting it as original. - No owner-private names, products, or internal paths in skill or agent text — this ships to every installer. `scripts/check-private-terms.sh` enforces this against - `scripts/private-terms.txt`. + `scripts/private-terms.txt`, plus a private list the maintainers hold. A PR can + also get a warning that a line may use a private product's vocabulary: that + isn't a failure, but check the line was written for this repo and not lifted + from elsewhere. ## Running the checks locally diff --git a/plugins/hardening/skills/privacy-audit/SKILL.md b/plugins/hardening/skills/privacy-audit/SKILL.md index aeae529..597310f 100644 --- a/plugins/hardening/skills/privacy-audit/SKILL.md +++ b/plugins/hardening/skills/privacy-audit/SKILL.md @@ -15,11 +15,11 @@ The rule is explicit: **a privacy policy is required if any user data is collect - Any Zod schema in a route handler with fields beyond `id`/`createdAt` — `grep -rn "z.object" --include="*.ts"` and read what each schema captures. - Stripe customer/subscription metadata (`stripe.customers.create`, `metadata:` blocks) — Stripe stores whatever you attach. - PostHog `capture()`/`identify()` calls — check `properties:` payloads for anything beyond anonymous event data (email, name, IP if not stripped). - - File uploads (campaign assets, avatars) — where do they land (S3/R2/local disk) and is there EXIF/metadata scrubbing? + - File uploads (user documents, avatars) — where do they land (S3/R2/local disk) and is there EXIF/metadata scrubbing? - BYOK products: the user's own Anthropic/OpenAI API key — this is the most sensitive field in the product. Confirm it is encrypted at rest, never logged, and never returned in any API response after initial save. 2. **Build the data map.** For every field found, record: field name → source (which form/event) → storage location (table.column, PostHog event property, Stripe metadata key, log line) → retention (indefinite / N days / until account deletion) → who else sees it (third-party processor: Stripe, PostHog, email provider, Sentry/Better Stack). - - Table format works well here — one row per data category (email, name, BYOK key, IP, campaign content, payment method) with those five columns. + - Table format works well here — one row per data category (email, name, BYOK key, IP, user-created content, payment method) with those five columns. 3. **Check privacy-policy alignment.** Read the live privacy policy (or draft from `legal-docs` skill if none exists) and confirm every row in the data map is disclosed. Flag: - Data collected but not mentioned in the policy (the common miss: PostHog session recordings, IP addresses in logs, third-party sub-processors not listed). diff --git a/scripts/check-private-terms.sh b/scripts/check-private-terms.sh index 161c488..52ab688 100755 --- a/scripts/check-private-terms.sh +++ b/scripts/check-private-terms.sh @@ -26,6 +26,15 @@ # repository secret for CI on the owner's own pushes/PRs. Empty on # fork PRs, which then run with the public list only. # +# Warning tier: an overlay line that starts with "warn:" is a warning, not a +# failure. It is for a private product's VOCABULARY (its domain nouns), which +# names miss: a worked example lifted from the product can leak it without +# naming it. Some of those words are also ordinary English, so a hit asks a +# person to look rather than blocking the PR. In GitHub Actions each hit +# becomes a ::warning annotation on the PR (file and line only); locally the +# matching line is printed too, since a local terminal is private. Warning +# patterns never run against docs/. +# # One extra check: docs/ (internal planning material, never shipped) is # scanned against ONLY the local/secret overlay, never the public list -- # docs/ legitimately discusses the owner's private products by name, so the @@ -64,6 +73,20 @@ fi cat "$OVERLAY_FILE" >> "$PATTERNS_FILE" fail=0 +warned=0 + +report_warning() { + # $1 = grep hits (file:line:text), never echoed to CI logs with the text + echo "$1" | while IFS= read -r hit; do + file=$(echo "$hit" | cut -d: -f1) + line=$(echo "$hit" | cut -d: -f2) + if [ "${GITHUB_ACTIONS:-}" = "true" ]; then + echo "::warning file=$file,line=$line::Possible private-product vocabulary (from the private list). Check this line was not lifted from a private product." + else + echo " $hit" >&2 + fi + done +} : > "$SCAN_LIST_ALL" : > "$SCAN_LIST_NO_README" @@ -77,6 +100,14 @@ if [ -s "$SCAN_LIST_ALL" ]; then while IFS= read -r pattern; do case "$pattern" in ''|'#'*) continue ;; + warn:*) + hits=$(xargs -0 grep -rniIE "${pattern#warn:}" < "$SCAN_LIST_ALL" 2>/dev/null || true) + if [ -n "$hits" ]; then + echo "WARNING: private-product vocabulary (from the private list) found; check these lines:" >&2 + report_warning "$hits" + warned=1 + fi + continue ;; esac if [ "$pattern" = "Stylus Nexus" ]; then scan_list="$SCAN_LIST_NO_README" @@ -110,6 +141,7 @@ if [ -d docs ] && [ -s "$OVERLAY_FILE" ]; then case "$pattern" in ''|'#'*) continue ;; esac + case "$pattern" in warn:*) continue ;; esac hits=$(grep -rniIE "$pattern" docs 2>/dev/null || true) if [ -n "$hits" ]; then echo "PRIVATE TERM (from the private list) found in docs/ at:" >&2 @@ -121,5 +153,6 @@ elif [ -d docs ]; then echo "no private-terms overlay available -- skipping docs/ check" >&2 fi +[ "$warned" = 1 ] && echo "warnings above are not failures: confirm each line is generic, or rewrite it" >&2 [ "$fail" = 0 ] && echo "no private terms found in plugins/*/skills, plugins/*/agents, plugins/*/README.md, or docs/" exit "$fail"