Skip to content

Commit 4475b90

Browse files
SchenLongclaude
andcommitted
feat: Daitenguyama-Shrine — Kagami fingerprint, Shingan scanner, fixture expansion, security hardening
4-pillar update (15 epics, ~98 stories, 4 phases): Pillar A — Market Parity (D1-D6): - LLM quality metrics, executive summary PDF/Markdown export - Sengoku campaign engine with skill chaining and conditional branching - Compliance export API, consolidated report with shingan section Pillar B — Kagami (K1-K7): - LLM behavioral fingerprinting engine with 18 probes - Feature vector extraction, cosine distance matching, signature DB - KagamiPanel, KagamiResults, ProbeProgress, FeatureRadar components - API route /api/llm/fingerprint with streaming support Pillar C — Shingan (D7): - Universal skill/agent scanner with 6-layer attack taxonomy - Trust scoring (0-100) across payload, metadata, context, social, exfil, supply-chain - skill-parser.ts supporting 6 formats (MCP, Claude agent, BMAD, hooks, plugin, unknown) - CLI --skill mode with realpath containment and extension guard Pillar D — Fixture Expansion (D8): - 600+ new fixtures across 36 attack categories (high + info severity) - Video, audio, document, image, translation, cognitive, environmental categories - Fixture validation tests ensuring scanner pass-through accuracy Security hardening (9 findings fixed): - CLI output path traversal → enforced CWD boundary - Symlink traversal in walkDir → skip symlinks - ReDoS via glob → max length + wildcard collapsing - Prototype pollution in skill-parser → Object.create(null) + deny-list - SSRF payload surface → 50KB max payload guard - --skill symlink attack → realpath + TPI_SKILL_ROOT containment - --skill binary file → extension whitelist guard - Shingan report symlink → realpathSync containment + field sanitization - Evidence directory filename → path separator rejection Tests: 6,210 passing (2,449 bu-tpi + 3,761 dojolm-web), 0 failures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d99e25c commit 4475b90

File tree

791 files changed

+39125
-240
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

791 files changed

+39125
-240
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
name: 'TPI Scan — AI Security Scanner'
2+
description: 'Scan files for prompt injection, supply chain attacks, and AI security threats'
3+
4+
inputs:
5+
path:
6+
description: 'Files or directory to scan'
7+
required: true
8+
format:
9+
description: 'Output format (sarif/junit/json/csv/text)'
10+
required: false
11+
default: 'sarif'
12+
threshold:
13+
description: 'Minimum severity to fail (CRITICAL/WARNING/INFO)'
14+
required: false
15+
default: 'CRITICAL'
16+
engines:
17+
description: 'Comma-separated engine filter (e.g., "core-patterns,mcp-parser")'
18+
required: false
19+
default: ''
20+
node-version:
21+
description: 'Node.js version'
22+
required: false
23+
default: '20'
24+
glob:
25+
description: 'Glob pattern for directory scanning'
26+
required: false
27+
default: '**/*.txt'
28+
29+
outputs:
30+
findings-count:
31+
description: 'Total number of findings'
32+
value: ${{ steps.scan.outputs.findings-count }}
33+
verdict:
34+
description: 'BLOCK or ALLOW'
35+
value: ${{ steps.scan.outputs.verdict }}
36+
report-path:
37+
description: 'Path to the generated report file'
38+
value: ${{ steps.scan.outputs.report-path }}
39+
40+
runs:
41+
using: 'composite'
42+
steps:
43+
- name: Setup Node.js
44+
uses: actions/setup-node@v4
45+
with:
46+
node-version: ${{ inputs.node-version }}
47+
48+
- name: Install dependencies
49+
shell: bash
50+
run: npm ci
51+
working-directory: ${{ github.workspace }}
52+
53+
- name: Run TPI Scan
54+
id: scan
55+
shell: bash
56+
env:
57+
INPUT_FORMAT: ${{ inputs.format }}
58+
INPUT_THRESHOLD: ${{ inputs.threshold }}
59+
INPUT_ENGINES: ${{ inputs.engines }}
60+
INPUT_PATH: ${{ inputs.path }}
61+
INPUT_GLOB: ${{ inputs.glob }}
62+
RUNNER_TEMP: ${{ runner.temp }}
63+
run: |
64+
# Validate format input
65+
case "${INPUT_FORMAT}" in
66+
sarif|junit|json|csv|text) ;;
67+
*) echo "::error::Invalid format: ${INPUT_FORMAT}" && exit 1 ;;
68+
esac
69+
70+
# Validate threshold input
71+
case "${INPUT_THRESHOLD}" in
72+
CRITICAL|WARNING|INFO) ;;
73+
*) echo "::error::Invalid threshold: ${INPUT_THRESHOLD}" && exit 1 ;;
74+
esac
75+
76+
# Determine report file extension
77+
case "${INPUT_FORMAT}" in
78+
sarif) EXT="sarif" ;;
79+
junit) EXT="xml" ;;
80+
csv) EXT="csv" ;;
81+
json) EXT="json" ;;
82+
*) EXT="txt" ;;
83+
esac
84+
85+
REPORT_PATH="${RUNNER_TEMP}/tpi-scan-report.${EXT}"
86+
ARGS="--format ${INPUT_FORMAT} --threshold ${INPUT_THRESHOLD} --output ${REPORT_PATH}"
87+
88+
if [ -n "${INPUT_ENGINES}" ]; then
89+
ARGS="${ARGS} --engines ${INPUT_ENGINES}"
90+
fi
91+
92+
if [ -d "${INPUT_PATH}" ]; then
93+
ARGS="${ARGS} --dir ${INPUT_PATH} --glob ${INPUT_GLOB}"
94+
else
95+
ARGS="${ARGS} --file ${INPUT_PATH}"
96+
fi
97+
98+
set +e
99+
npx tpi-scan ${ARGS} --summary 2>&1
100+
EXIT_CODE=$?
101+
set -e
102+
103+
if [ ${EXIT_CODE} -eq 0 ]; then
104+
echo "verdict=ALLOW" >> "${GITHUB_OUTPUT}"
105+
else
106+
echo "verdict=BLOCK" >> "${GITHUB_OUTPUT}"
107+
fi
108+
109+
echo "report-path=${REPORT_PATH}" >> "${GITHUB_OUTPUT}"
110+
111+
# Extract findings count from report
112+
if [ -f "${REPORT_PATH}" ]; then
113+
if [ "${INPUT_FORMAT}" = "json" ]; then
114+
COUNT=$(node -e "const r=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log(r.findings?.length??0)" "${REPORT_PATH}")
115+
elif [ "${INPUT_FORMAT}" = "sarif" ]; then
116+
COUNT=$(node -e "const r=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));console.log(r.runs?.[0]?.results?.length??0)" "${REPORT_PATH}")
117+
else
118+
COUNT="unknown"
119+
fi
120+
echo "findings-count=${COUNT}" >> "${GITHUB_OUTPUT}"
121+
else
122+
echo "findings-count=0" >> "${GITHUB_OUTPUT}"
123+
fi
124+
125+
exit ${EXIT_CODE}
126+
127+
- name: Upload SARIF to GitHub Code Scanning
128+
if: inputs.format == 'sarif' && always()
129+
uses: github/codeql-action/upload-sarif@v3
130+
with:
131+
sarif_file: ${{ steps.scan.outputs.report-path }}
132+
category: tpi-scan
133+
continue-on-error: true
134+
135+
- name: Upload report artifact
136+
if: always()
137+
uses: actions/upload-artifact@v4
138+
with:
139+
name: tpi-scan-report
140+
path: ${{ steps.scan.outputs.report-path }}
141+
retention-days: 30
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# TPI Scan — Makefile Targets
2+
# Copy to your project root or include via: include path/to/Makefile
3+
4+
TPI_DIR ?= .
5+
TPI_GLOB ?= **/*.{txt,md,json,yml,yaml}
6+
TPI_THRESHOLD ?= WARNING
7+
TPI_OUTPUT_DIR ?= ./reports
8+
9+
.PHONY: scan scan-sarif scan-junit scan-json scan-csv scan-dir
10+
11+
scan: ## Run TPI scan with human-readable output
12+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --threshold $(TPI_THRESHOLD)
13+
14+
scan-sarif: ## Run TPI scan with SARIF output
15+
@mkdir -p $(TPI_OUTPUT_DIR)
16+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --format sarif --output $(TPI_OUTPUT_DIR)/tpi-report.sarif --threshold $(TPI_THRESHOLD)
17+
@echo "Report: $(TPI_OUTPUT_DIR)/tpi-report.sarif"
18+
19+
scan-junit: ## Run TPI scan with JUnit XML output
20+
@mkdir -p $(TPI_OUTPUT_DIR)
21+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --format junit --output $(TPI_OUTPUT_DIR)/tpi-report.xml --threshold $(TPI_THRESHOLD)
22+
@echo "Report: $(TPI_OUTPUT_DIR)/tpi-report.xml"
23+
24+
scan-json: ## Run TPI scan with JSON output
25+
@mkdir -p $(TPI_OUTPUT_DIR)
26+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --format json --output $(TPI_OUTPUT_DIR)/tpi-report.json --threshold $(TPI_THRESHOLD)
27+
@echo "Report: $(TPI_OUTPUT_DIR)/tpi-report.json"
28+
29+
scan-csv: ## Run TPI scan with CSV output
30+
@mkdir -p $(TPI_OUTPUT_DIR)
31+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --format csv --output $(TPI_OUTPUT_DIR)/tpi-report.csv --threshold $(TPI_THRESHOLD)
32+
@echo "Report: $(TPI_OUTPUT_DIR)/tpi-report.csv"
33+
34+
scan-dir: ## Scan specific directory (usage: make scan-dir TPI_DIR=./prompts)
35+
npx tpi-scan --dir $(TPI_DIR) --glob "$(TPI_GLOB)" --threshold $(TPI_THRESHOLD) --summary
36+
37+
help: ## Show this help
38+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# TPI Scan — GitHub Actions Workflow Example
2+
# Copy this file to your repository's .github/workflows/ directory
3+
4+
name: AI Security Scan
5+
on:
6+
pull_request:
7+
branches: [main]
8+
push:
9+
branches: [main]
10+
11+
permissions:
12+
contents: read
13+
security-events: write
14+
pull-requests: write
15+
16+
jobs:
17+
tpi-scan:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Run TPI Scan (SARIF)
24+
id: scan
25+
uses: ./packages/bu-tpi/.github/actions/tpi-scan
26+
with:
27+
path: '.'
28+
format: sarif
29+
threshold: WARNING
30+
glob: '**/*.{txt,md,json,yml,yaml}'
31+
32+
- name: Run TPI Scan (JUnit)
33+
if: always()
34+
uses: ./packages/bu-tpi/.github/actions/tpi-scan
35+
with:
36+
path: '.'
37+
format: junit
38+
threshold: WARNING
39+
glob: '**/*.{txt,md,json,yml,yaml}'
40+
41+
- name: Comment on PR
42+
if: github.event_name == 'pull_request' && always()
43+
uses: actions/github-script@v7
44+
with:
45+
script: |
46+
const verdict = '${{ steps.scan.outputs.verdict }}';
47+
const count = '${{ steps.scan.outputs.findings-count }}';
48+
const icon = verdict === 'ALLOW' ? ':white_check_mark:' : ':x:';
49+
const body = `## ${icon} TPI Security Scan\n\n` +
50+
`**Verdict:** ${verdict}\n` +
51+
`**Findings:** ${count}\n\n` +
52+
`See the [Security tab](${context.payload.repository.html_url}/security/code-scanning) for details.`;
53+
github.rest.issues.createComment({
54+
issue_number: context.issue.number,
55+
owner: context.repo.owner,
56+
repo: context.repo.repo,
57+
body
58+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# TPI Scan — GitLab CI Configuration Example
2+
# Copy this file (or merge into your existing .gitlab-ci.yml)
3+
4+
stages:
5+
- test
6+
7+
tpi-scan:
8+
stage: test
9+
image: node:20-slim
10+
before_script:
11+
- npm ci
12+
script:
13+
- npx tpi-scan --dir . --glob "**/*.{txt,md,json,yml,yaml}" --format junit --output tpi-report.xml --threshold WARNING
14+
artifacts:
15+
when: always
16+
reports:
17+
junit: tpi-report.xml
18+
paths:
19+
- tpi-report.xml
20+
expire_in: 30 days
21+
allow_failure: false
22+
23+
tpi-scan-sarif:
24+
stage: test
25+
image: node:20-slim
26+
before_script:
27+
- npm ci
28+
script:
29+
- npx tpi-scan --dir . --glob "**/*.{txt,md,json,yml,yaml}" --format sarif --output tpi-report.sarif
30+
artifacts:
31+
when: always
32+
paths:
33+
- tpi-report.sarif
34+
expire_in: 30 days
35+
allow_failure: true
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env bash
2+
# TPI Scan — Git Pre-Commit Hook
3+
#
4+
# Installation:
5+
# cp pre-commit-hook.sh .git/hooks/pre-commit
6+
# chmod +x .git/hooks/pre-commit
7+
#
8+
# Configuration (optional): create .tpi-scan.yml in project root
9+
# threshold: WARNING
10+
# patterns:
11+
# - "*.txt"
12+
# - "*.md"
13+
# - "*.json"
14+
# - "*.yml"
15+
# - "*.yaml"
16+
17+
set -euo pipefail
18+
19+
# Defaults
20+
THRESHOLD="CRITICAL"
21+
PATTERNS=("*.txt" "*.json" "*.md" "*.yml" "*.yaml")
22+
23+
# Load config if present
24+
CONFIG_FILE=".tpi-scan.yml"
25+
if [ -f "$CONFIG_FILE" ]; then
26+
# Parse threshold
27+
CFG_THRESHOLD=$(grep -E '^threshold:' "$CONFIG_FILE" | awk '{print $2}' | tr -d '"'"'" || true)
28+
if [ -n "$CFG_THRESHOLD" ]; then
29+
case "$CFG_THRESHOLD" in
30+
CRITICAL|WARNING|INFO) THRESHOLD="$CFG_THRESHOLD" ;;
31+
*) echo "[TPI Scan] Invalid threshold '${CFG_THRESHOLD}' in config, using default: CRITICAL" >&2 ;;
32+
esac
33+
fi
34+
35+
# Parse patterns (simple YAML array parsing)
36+
CFG_PATTERNS=()
37+
IN_PATTERNS=false
38+
while IFS= read -r line; do
39+
if echo "$line" | grep -qE '^patterns:'; then
40+
IN_PATTERNS=true
41+
continue
42+
fi
43+
if [ "$IN_PATTERNS" = true ]; then
44+
if echo "$line" | grep -qE '^\s+-\s+'; then
45+
PATTERN=$(echo "$line" | sed 's/^\s*-\s*//' | tr -d '"'"'")
46+
CFG_PATTERNS+=("$PATTERN")
47+
else
48+
IN_PATTERNS=false
49+
fi
50+
fi
51+
done < "$CONFIG_FILE"
52+
53+
if [ ${#CFG_PATTERNS[@]} -gt 0 ]; then
54+
PATTERNS=("${CFG_PATTERNS[@]}")
55+
fi
56+
fi
57+
58+
# Collect staged files matching patterns
59+
STAGED_FILES=()
60+
for PATTERN in "${PATTERNS[@]}"; do
61+
while IFS= read -r file; do
62+
if [ -n "$file" ] && [ -f "$file" ]; then
63+
STAGED_FILES+=("$file")
64+
fi
65+
done < <(git diff --cached --name-only --diff-filter=ACM -- "$PATTERN" 2>/dev/null || true)
66+
done
67+
68+
# Nothing to scan
69+
if [ ${#STAGED_FILES[@]} -eq 0 ]; then
70+
exit 0
71+
fi
72+
73+
echo "[TPI Scan] Scanning ${#STAGED_FILES[@]} staged file(s) (threshold: ${THRESHOLD})..."
74+
75+
# Scan each file
76+
BLOCK=false
77+
TOTAL_FINDINGS=0
78+
79+
for FILE in "${STAGED_FILES[@]}"; do
80+
RESULT=$(npx tpi-scan --file "$FILE" --format json --threshold "$THRESHOLD" 2>/dev/null || true)
81+
if [ -n "$RESULT" ]; then
82+
FINDINGS=$(echo "$RESULT" | node -e "
83+
let d='';process.stdin.on('data',c=>d+=c);
84+
process.stdin.on('end',()=>{
85+
try{const r=JSON.parse(d);console.log(r.findings?.length??0)}
86+
catch{console.log(0)}
87+
})" 2>/dev/null || echo "0")
88+
VERDICT=$(echo "$RESULT" | node -e "
89+
let d='';process.stdin.on('data',c=>d+=c);
90+
process.stdin.on('end',()=>{
91+
try{const r=JSON.parse(d);console.log(r.verdict??'ALLOW')}
92+
catch{console.log('ALLOW')}
93+
})" 2>/dev/null || echo "ALLOW")
94+
95+
if [ "$FINDINGS" != "0" ]; then
96+
TOTAL_FINDINGS=$((TOTAL_FINDINGS + FINDINGS))
97+
echo " ${FILE}: ${FINDINGS} finding(s) [${VERDICT}]"
98+
fi
99+
100+
if [ "$VERDICT" = "BLOCK" ]; then
101+
BLOCK=true
102+
fi
103+
fi
104+
done
105+
106+
if [ "$BLOCK" = true ]; then
107+
echo ""
108+
echo "[TPI Scan] BLOCKED: ${TOTAL_FINDINGS} finding(s) exceed ${THRESHOLD} threshold."
109+
echo " Run 'npx tpi-scan --file <path>' for details."
110+
echo " Use 'git commit --no-verify' to skip (not recommended)."
111+
exit 1
112+
else
113+
if [ "$TOTAL_FINDINGS" -gt 0 ]; then
114+
echo "[TPI Scan] PASSED with ${TOTAL_FINDINGS} finding(s) below ${THRESHOLD} threshold."
115+
fi
116+
exit 0
117+
fi

0 commit comments

Comments
 (0)