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
69 changes: 69 additions & 0 deletions .claude/scripts/check-disclosure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# check-disclosure.sh — open-source disclosure hygiene.
# Detects SAP-internal URLs, ORD IDs, internal Jira, unfilled PR body templates.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/json-emit.sh
source "$SCRIPT_DIR/lib/json-emit.sh"
# shellcheck source=lib/skill-self-skip.sh
source "$SCRIPT_DIR/lib/skill-self-skip.sh"

LANGUAGE="${LANGUAGE:-python}"
PROFILE="${DISCLOSURE_PROFILE:-public}" # "public" or "internal"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"
PR_BODY_FILE="${PR_BODY_FILE:-}"

STARTED=$(now_iso)
findings=$(mktemp)
trap 'rm -f "$findings"' EXIT

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

# Determine severity based on profile
sev_public() { if [ "$PROFILE" = "public" ]; then echo "BLOCK"; else echo "FLAG"; fi; }
sev_internal_ok() { if [ "$PROFILE" = "public" ]; then echo "BLOCK"; else echo "PASS"; fi; }

# Scan added lines only
echo "$diff_content" | awk '
BEGIN { file=""; line=0 }
/^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next }
/^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next }
/^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next }
/^ / { line++; next }
' | while IFS=$'\t' read -r file line_num content; do
[ -z "$file" ] && continue
# Self-review protection: skip skill files
if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi

# DIS-02: Internal Jira URL (checked first — more specific)
dis02_fired=false
if echo "$content" | grep -qEi 'jira\.tools\.sap|jira\.wdf\.sap\.corp'; then
emit_finding "DIS-02" "$(sev_public)" "$file" "$line_num" "Internal Jira URL — remove or move to internal-only docs" "" >> "$findings"
dis02_fired=true
fi
# DIS-01: SAP-internal hostnames (skip if DIS-02 already covers the same line)
if [ "$dis02_fired" = "false" ] && echo "$content" | grep -qEi '(int\.repositories\.cloud\.sap|\.tools\.sap|\.wdf\.sap\.corp|\.mo\.sap\.corp)'; then
emit_finding "DIS-01" "$(sev_public)" "$file" "$line_num" "SAP-internal hostname detected in code" "" >> "$findings"
fi
# DIS-06: Internal artifactory index-url
if echo "$content" | grep -qEi '\-\-index-url[[:space:]]+https?://int\.repositories\.cloud\.sap'; then
emit_finding "DIS-06" "BLOCK" "$file" "$line_num" "Internal artifactory --index-url — must not appear in public/shared configs" "" >> "$findings"
fi
done

# DIS-07/08: PR body
if [ -n "$PR_BODY_FILE" ] && [ -f "$PR_BODY_FILE" ]; then
body=$(cat "$PR_BODY_FILE")
# DIS-07: unfilled Closes #<issue_number> placeholder
if echo "$body" | grep -qE 'Closes #<issue_number>'; then
emit_finding "DIS-07" "BLOCK" "PR_BODY" 1 "PR body contains unfilled 'Closes #<issue_number>' placeholder" "" >> "$findings"
fi
# DIS-08 (SHADOW): internal URLs / Jira in PR body
if echo "$body" | grep -qEi '\.tools\.sap|\.wdf\.sap\.corp|jira\.tools\.sap'; then
emit_finding "DIS-08" "FLAG" "PR_BODY" 1 "PR body references SAP-internal URLs/Jira — remove from public-visible artifacts" "" >> "$findings"
fi
fi

status=$(status_from_findings < "$findings")
emit_report "disclosure" "$LANGUAGE" "$status" "$STARTED" < "$findings"
74 changes: 74 additions & 0 deletions .claude/scripts/check-hardcode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# check-hardcode.sh — no hardcoded URLs, credentials, or magic values in impl code.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/json-emit.sh
source "$SCRIPT_DIR/lib/json-emit.sh"
# shellcheck source=lib/skill-self-skip.sh
source "$SCRIPT_DIR/lib/skill-self-skip.sh"

LANGUAGE="${LANGUAGE:-python}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

STARTED=$(now_iso)
findings=$(mktemp)
trap 'rm -f "$findings"' EXIT

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

# Determine ignore patterns per language
if [ "$LANGUAGE" = "python" ]; then
ignore_files='^(tests?/|mocks?/|docs?/|.*/spec/|.*/constants\.py|.*/user-guide\.md|README\.md)'
else
ignore_files='^(src/test/|mocks?/|docs?/|.*Constants\.java|.*/constants/|.*/user-guide\.md|README\.md)'
fi

echo "$diff_content" | awk '
BEGIN { file=""; line=0 }
/^diff --git a\// { file=$4; sub(/^b\//, "", file); line=0; next }
/^@@/ { if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0; next }
/^\+/ && !/^\+\+\+/ { print file "\t" line "\t" substr($0, 2); line++; next }
/^ / { line++; next }
' | while IFS=$'\t' read -r file line_num content; do
[ -z "$file" ] && continue
if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi
# Filter out test/mock/docs/constants files
if echo "$file" | grep -qE "$ignore_files"; then continue; fi

# HC-01: hardcoded URL. Extract each URL and check individually so a line
# with both example.com (allowed) and api.com (real) still fires on the real one.
# Use word boundary via (^|[^A-Za-z0-9]) so we don't match tokens inside identifiers.
while IFS= read -r url; do
[ -z "$url" ] && continue
# allow-list: only IANA-reserved test/example TLDs and localhost
# `.example` must be the terminal label (RFC 2606) — anchor at path/port/end.
if echo "$url" | grep -qE '^https?://(localhost|127\.0\.0\.1|example\.(com|org|net)|[^/]+\.example(/|:|$)|reserved\.)'; then
continue
fi
emit_finding "HC-01" "BLOCK" "$file" "$line_num" "Hardcoded URL '$url' in implementation — externalize to config" "" >> "$findings"
break # only one finding per line to avoid duplicate reports
done < <(echo "$content" | grep -oE 'https?://[A-Za-z0-9][A-Za-z0-9._~:/?#@!$&*+,;=%-]*' || true)
# HC-02: Authorization Bearer
if echo "$content" | grep -qE 'Authorization[[:space:]]*:[[:space:]]*Bearer[[:space:]]+[A-Za-z0-9]'; then
emit_finding "HC-02" "BLOCK" "$file" "$line_num" "Hardcoded Authorization header value" "" >> "$findings"
fi
# HC-04: direct os.environ / System.getenv
if [ "$LANGUAGE" = "python" ]; then
if echo "$content" | grep -qE 'os\.environ\["[A-Z]'; then
emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct os.environ access — prefer secret_resolver / config layer" "" >> "$findings"
fi
else
if echo "$content" | grep -qE 'System\.getenv\('; then
emit_finding "HC-04" "FLAG" "$file" "$line_num" "Direct System.getenv() — prefer SecretResolver" "" >> "$findings"
fi
fi
# HC-06: hardcoded timeout numeric literal
# Use word boundary via (^|[^A-Za-z0-9_]) so 'default_timeout' or 'my_timeout' don't match
if echo "$content" | grep -qiE '(^|[^A-Za-z0-9_])(timeout|retries|max_retries)[[:space:]]*=[[:space:]]*[0-9]+'; then
emit_finding "HC-06" "FLAG" "$file" "$line_num" "Hardcoded timeout/retry number — externalize to config" "" >> "$findings"
fi
done

status=$(status_from_findings < "$findings")
emit_report "hardcode" "$LANGUAGE" "$status" "$STARTED" < "$findings"
87 changes: 87 additions & 0 deletions .claude/scripts/check-license-spdx.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# check-license-spdx.sh — verify new source files have SPDX headers.
# REUSE.toml aggregate → rule PASSES (baseline exemption).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/json-emit.sh
source "$SCRIPT_DIR/lib/json-emit.sh"
# shellcheck source=lib/predicates.sh
source "$SCRIPT_DIR/lib/predicates.sh"

LANGUAGE="${LANGUAGE:-python}"
REPO_ROOT="${REPO_ROOT:-.}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

STARTED=$(now_iso)
findings=$(mktemp)
trap 'rm -f "$findings"' EXIT

# Predicate: REUSE.toml aggregate present?
reuse_present=$(reuse_toml_aggregate_present "$REPO_ROOT")

diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

if [ "$reuse_present" = "true" ]; then
# Baseline exemption: rule OFF for this repo. Emit a valid report matching
# the JSON contract in 00-COMMON.md §3 (aggregators expect these fields).
jq -n --arg check "license-spdx" --arg lang "$LANGUAGE" --arg started "$STARTED" \
'{
check: $check,
version: "1.0.0",
language: $lang,
status: "PASS",
started_at: $started,
duration_ms: 0,
modules_analysed: [],
findings: [],
summary: {
block_count: 0,
flag_count: 0,
suppressed_by_baseline: 0,
pass_criteria_met: ["REUSE.toml aggregate present — LIC-01/02 exempted"],
pass_criteria_failed: []
}
}'
exit 0
fi

# Find newly added files (starts with "diff --git a/... b/..." followed by "new file mode")
# We match on "+++ b/<path>" lines that appear right after "new file mode"
added_files=$(echo "$diff_content" | awk '
/^diff --git/ { in_block=1; is_new=0; path=""; next }
in_block && /^new file mode/ { is_new=1; next }
in_block && /^\+\+\+ b\// { if (is_new) print substr($0, 7); in_block=0 }
')

# REUSE-IgnoreStart
if [ "$LANGUAGE" = "python" ]; then
ext_match='\.py$'
spdx_line='# SPDX-License-Identifier: Apache-2.0'
cprt_line='# SPDX-FileCopyrightText:'
else
ext_match='\.java$'
spdx_line='// SPDX-License-Identifier: Apache-2.0'
cprt_line='// SPDX-FileCopyrightText:'
fi
# REUSE-IgnoreEnd

while IFS= read -r f; do
[ -z "$f" ] && continue
if ! echo "$f" | grep -qE "$ext_match"; then continue; fi
# Read first 10 lines from the diff for that file to check headers
header=$(echo "$diff_content" | awk -v file="$f" '
$0 == "+++ b/" file { flag=1; count=0; next }
flag && /^\+/ && !/^\+\+\+/ { print substr($0, 2); count++; if (count>=10) exit }
flag && /^diff --git/ { exit }
')
if ! echo "$header" | grep -qF "$spdx_line"; then
emit_finding "LIC-01" "BLOCK" "$f" 1 "New source file missing SPDX-License-Identifier header" "" >> "$findings"
fi
if ! echo "$header" | grep -qF "$cprt_line"; then
emit_finding "LIC-02" "BLOCK" "$f" 1 "New source file missing SPDX-FileCopyrightText header" "" >> "$findings"
fi
done <<< "$added_files"

status=$(status_from_findings < "$findings")
emit_report "license-spdx" "$LANGUAGE" "$status" "$STARTED" < "$findings"
103 changes: 103 additions & 0 deletions .claude/scripts/check-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# check-secrets.sh — detect secrets (AWS keys, JWTs, GitHub tokens, private keys, etc.) in added lines.
# All SEC-* rules are BLOCK_LOCKED (cannot be suppressed).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/json-emit.sh
source "$SCRIPT_DIR/lib/json-emit.sh"
# shellcheck source=lib/skill-self-skip.sh
source "$SCRIPT_DIR/lib/skill-self-skip.sh"

LANGUAGE="${LANGUAGE:-python}"
DIFF_FILE="${DIFF_FILE:-/dev/stdin}"

STARTED=$(now_iso)

# Read diff (either from env-provided file or stdin) — extract added lines with file+line info
diff_content=$(cat "$DIFF_FILE" 2>/dev/null || echo "")

if [ -z "$diff_content" ]; then
emit_report "secrets" "$LANGUAGE" "PASS" "$STARTED" <<< ""
exit 0
fi

findings=$(mktemp)
trap 'rm -f "$findings"' EXIT

# Parse diff and scan each added line
echo "$diff_content" | awk '
BEGIN { file=""; line=0 }
/^diff --git a\// {
file=$4
sub(/^b\//, "", file)
line=0
next
}
/^@@/ {
if (match($0, /\+[0-9]+/)) line=substr($0, RSTART+1, RLENGTH-1)+0
next
}
/^\+/ && !/^\+\+\+/ {
print file "\t" line "\t" substr($0, 2)
line++
next
}
/^ / { line++; next }
' | while IFS=$'\t' read -r file line_num content; do
[ -z "$file" ] && continue
# Self-review protection: skip skill files
if [ "$(is_skill_file "$file")" = "true" ]; then continue; fi

# SEC-01: AWS Access Key
if echo "$content" | grep -qE 'AKIA[0-9A-Z]{16}'; then
emit_finding "SEC-01" "BLOCK" "$file" "$line_num" "AWS Access Key detected — remove immediately and rotate the key" "" >> "$findings"
fi
# SEC-02: Google API Key
if echo "$content" | grep -qE 'AIza[0-9A-Za-z_-]{35}'; then
emit_finding "SEC-02" "BLOCK" "$file" "$line_num" "Google API Key detected — remove and rotate" "" >> "$findings"
fi
# SEC-03: GitHub PAT
if echo "$content" | grep -qE 'gh[pousr]_[A-Za-z0-9_]{36,}'; then
emit_finding "SEC-03" "BLOCK" "$file" "$line_num" "GitHub PAT detected — remove and rotate" "" >> "$findings"
fi
# SEC-04: OpenAI / Anthropic API key (both use sk-… prefix; Anthropic allows dashes)
if echo "$content" | grep -qE 'sk-[A-Za-z0-9_-]{20,}'; then
emit_finding "SEC-04" "BLOCK" "$file" "$line_num" "AI provider API key detected (sk-…) — remove and rotate" "" >> "$findings"
fi
# SEC-05: Slack bot token
if echo "$content" | grep -qE 'xox[baprs]-[A-Za-z0-9-]+'; then
emit_finding "SEC-05" "BLOCK" "$file" "$line_num" "Slack token detected — remove and rotate" "" >> "$findings"
fi
# SEC-06: JWT
if echo "$content" | grep -qE 'eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'; then
emit_finding "SEC-06" "BLOCK" "$file" "$line_num" "JWT token detected — remove and rotate" "" >> "$findings"
fi
# SEC-07: Private key header
if echo "$content" | grep -qE 'BEGIN (RSA |EC |OPENSSH |DSA |ENCRYPTED )?PRIVATE KEY'; then
emit_finding "SEC-07" "BLOCK" "$file" "$line_num" "Private key header detected — remove and rotate" "" >> "$findings"
fi
# SEC-08: BTP client_secret literal (heuristic: assignment with looks-like-secret value)
# Match client_secret= or clientsecret= with quoted values that look like real secrets
if echo "$content" | grep -qE '"clientsecret"[[:space:]]*:[[:space:]]*"[A-Za-z0-9+/=]{20,}"'; then
emit_finding "SEC-08" "BLOCK" "$file" "$line_num" "BTP client_secret literal detected — use secret resolver" "" >> "$findings"
fi
done

# SEC-10: .env files in diff (not .env.example, .env.test, .env.sample, .env.template)
# Match both top-level .env and subdirectory service/.env
env_lines=$(echo "$diff_content" | grep -E '^\+\+\+ b/(.*/)?\.env(\..+)?$' || true)
while IFS= read -r line; do
[ -z "$line" ] && continue
# extract path from "+++ b/<path>"
path="${line#+++ b/}"
# basename check for allowlist
base="${path##*/}"
case "$base" in
.env.example|.env.test|.env.sample|.env.template) continue ;;
.env|.env.*) emit_finding "SEC-10" "BLOCK" "$path" 1 ".env file committed — never commit .env; use .env.example" "" >> "$findings" ;;
esac
done <<< "$env_lines"

status=$(status_from_findings < "$findings")
emit_report "secrets" "$LANGUAGE" "$status" "$STARTED" < "$findings"
Loading
Loading