chore: import upstream snapshot with attribution
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:08:55 +08:00
commit 7a0da7932b
2985 changed files with 1049377 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
#!/bin/bash
# Security check for potential unencrypted file writes to disk
# This script helps prevent accidentally bypassing encryption at rest
set -e
echo "Checking for potential unencrypted file writes to disk..."
echo "========================================="
# Patterns that might indicate writing sensitive data to disk
# Note: Using basic grep patterns without lookaheads
SUSPICIOUS_PATTERNS=(
# Python patterns
"\.write\("
"\.save\("
"\.dump\("
"open\(.*['\"]w['\"].*\)"
"open\(.*['\"]wb['\"].*\)"
"with.*open\(.*['\"]w['\"]"
"with.*open\(.*['\"]wb['\"]"
"\.to_csv\("
"\.to_json\("
"\.to_excel\("
"\.to_pickle\("
"tempfile\.NamedTemporaryFile.*delete=False"
"Path.*\.write_text\("
"Path.*\.write_bytes\("
"shutil\.copy"
"shutil\.move"
"\.export_to_file\("
"\.save_to_file\("
"\.write_pdf\("
"\.savefig\("
# JavaScript patterns
"fs\.writeFile"
"fs\.writeFileSync"
"fs\.createWriteStream"
"fs\.appendFile"
)
# Directories to exclude from checks
EXCLUDE_DIRS=(
"tests"
"test"
"__pycache__"
".git"
"node_modules"
".venv"
"venv"
"migrations"
"static"
"vendor"
"dist"
"build"
".next"
"coverage"
"examples"
"scripts"
".github"
"cookiecutter-docker"
)
# Files to exclude
EXCLUDE_FILES=(
"*_test.py"
"test_*.py"
"*.test.js"
"*.spec.js"
"*.test.ts"
"*.spec.ts"
"setup.py"
"webpack.config.js"
"**/migrations/*.py"
"*.min.js"
"*.bundle.js"
"*-min.js"
"*.min.css"
)
# Safe keywords that indicate encrypted or safe operations
# These patterns indicate that file writes have been security-verified
SAFE_KEYWORDS=(
"write_file_verified"
"write_json_verified"
)
# Known safe usage patterns (logs, configs, etc.)
SAFE_USAGE_PATTERNS=(
"security/file_write_verifier.py"
"import tempfile"
"tempfile\.mkdtemp"
"tmp_path"
"tmp_file"
)
# Build exclude arguments for grep
EXCLUDE_ARGS=""
for dir in "${EXCLUDE_DIRS[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude-dir=$dir"
done
for file in "${EXCLUDE_FILES[@]}"; do
EXCLUDE_ARGS="$EXCLUDE_ARGS --exclude=$file"
done
# Track if we found any issues
FOUND_ISSUES=0
ALL_MATCHES=""
echo "Scanning codebase for suspicious patterns..."
# Search only in src/ directory to avoid .venv and other non-source directories
SEARCH_PATHS="src/"
# Single pass to collect all matches
for pattern in "${SUSPICIOUS_PATTERNS[@]}"; do
# Use grep with binary files excluded and max line length to avoid issues with minified files
# shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS
matches=$(grep -rn -I $EXCLUDE_ARGS -- "$pattern" $SEARCH_PATHS --include="*.py" --include="*.js" --include="*.ts" 2>/dev/null | head -1000 || true)
if [ -n "$matches" ]; then
ALL_MATCHES="$ALL_MATCHES$matches\n"
fi
done
# Also check for specific problematic patterns in one pass
# shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS
temp_matches=$(grep -rn -I $EXCLUDE_ARGS -E "tmp_path|tempfile|/tmp/" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true)
if [ -n "$temp_matches" ]; then
ALL_MATCHES="$ALL_MATCHES$temp_matches\n"
fi
# shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS
db_matches=$(grep -rn -I $EXCLUDE_ARGS -E "report_content.*open|report_content.*write|markdown_content.*open|markdown_content.*write" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true)
if [ -n "$db_matches" ]; then
ALL_MATCHES="$ALL_MATCHES$db_matches\n"
fi
# shellcheck disable=SC2086 # Word splitting is intentional for EXCLUDE_ARGS
export_matches=$(grep -rn -I $EXCLUDE_ARGS -E "export.*Path|export.*path\.open|export.*\.write" $SEARCH_PATHS --include="*.py" 2>/dev/null | head -500 || true)
if [ -n "$export_matches" ]; then
ALL_MATCHES="$ALL_MATCHES$export_matches\n"
fi
# Now filter all matches at once
if [ -n "$ALL_MATCHES" ]; then
echo "Filtering results for false positives..."
# Remove duplicates and sort (use tr to handle potential null bytes)
ALL_MATCHES=$(echo -e "$ALL_MATCHES" | tr -d '\0' | sort -u)
filtered_matches=""
while IFS= read -r line; do
[ -z "$line" ] && continue
# Check if line contains safe keywords
skip_line=0
for safe_pattern in "${SAFE_KEYWORDS[@]}"; do
if echo "$line" | grep -qE -- "$safe_pattern"; then
skip_line=1
break
fi
done
# Check if line contains safe usage patterns
if [ "$skip_line" -eq 0 ]; then
for usage_pattern in "${SAFE_USAGE_PATTERNS[@]}"; do
if echo "$line" | grep -qE -- "$usage_pattern"; then
skip_line=1
break
fi
done
fi
# Additional filters for test/mock files that might not be caught by path exclusion
if [ "$skip_line" -eq 0 ]; then
if echo "$line" | grep -qE "test|mock|stub" && ! echo "$line" | grep -q "#"; then
skip_line=1
fi
fi
# Allowlist of files that legitimately write to disk without encryption.
# These must NOT touch user data or secrets — only:
# - web/app_factory.py — Flask/framework config writes
# - document_loaders/bytes_loader.py — in-memory → tmp for parsers
# - journal_quality/downloader.py — public OpenAlex/DOAJ/predatory/
# JabRef/ROR snapshots downloaded to the user data dir (bibliographic
# metadata only — journal names, ISSNs, h-indices; no PII/secrets)
# - journal_quality/data_sources/*.py — same family, per-source adapters
# that write the intermediate JSON manifests under the user data dir
# If you add an entry here, document WHY the file's writes are safe
# (public data, not user-specific, not encrypted at rest by design).
if [ "$skip_line" -eq 0 ]; then
if echo "$line" | grep -qE "web/app_factory\.py|document_loaders/bytes_loader\.py|journal_quality/downloader\.py|journal_quality/data_sources/.+\.py"; then
skip_line=1
fi
fi
# Filter safe temp files with proper cleanup
if [ "$skip_line" -eq 0 ]; then
if echo "$line" | grep -q "database/encrypted_db.py"; then
skip_line=1
fi
fi
if [ "$skip_line" -eq 0 ]; then
filtered_matches="$filtered_matches$line\n"
FOUND_ISSUES=1
fi
done <<< "$ALL_MATCHES"
if [ -n "$filtered_matches" ] && [ "$FOUND_ISSUES" -eq 1 ]; then
echo "⚠️ Found potential unencrypted file writes:"
echo "========================================="
echo -e "$filtered_matches"
fi
fi
echo "========================================="
if [ $FOUND_ISSUES -eq 1 ]; then
echo "❌ Security check failed: Found potential unencrypted file writes"
echo ""
echo "Please review the above findings and ensure:"
echo "1. Sensitive data is not written to disk unencrypted"
echo "2. Temporary files are properly cleaned up"
echo "3. Use in-memory operations where possible"
echo "4. If file writes are necessary, ensure they're encrypted or add '# Safe: <reason>' comment"
echo ""
echo "For exports, use the in-memory pattern like in export_report_to_memory()"
exit 1
else
echo "✅ Security check passed: No suspicious unencrypted file writes detected"
fi
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
#
# Combine multiple per-model AI code reviews into a single sticky PR comment.
#
# This is the pure-assembly half of the AI Code Reviewer workflow
# (.github/workflows/ai-code-reviewer.yml), split out so the non-trivial parts —
# header/footer stripping, "Reviewer N" section assembly, label union, and the
# pass/fail aggregation — can be unit-tested without any network or GitHub API
# access. The workflow runs the models (network) and writes each reviewer's raw
# stdout to resp_<i>.json and its exit code to code_<i>; this script turns those
# into the comment body, label set, decision, and success count.
#
# Usage:
# combine-ai-reviews.sh <work_dir> <model_1> [<model_2> ...]
#
# Reads, for i in 0..N-1 (N = number of model args):
# <work_dir>/resp_<i>.json - raw stdout from ai-reviewer.sh for reviewer i
# <work_dir>/code_<i> - that reviewer's integer exit code
# <work_dir>/err_<i>.log - that reviewer's stderr (only shown in DEBUG)
# The model names are used only for the per-reviewer log-group labels; the
# posted comment stays anonymized ("Reviewer 1", "Reviewer 2", ...).
#
# Environment:
# HEAD_SHA - head commit sha for the "Last reviewed at commit" line (opt)
# DEBUG_MODE - "true" to echo raw responses + stderr to this script's stderr
#
# Writes (into <work_dir>):
# comment_body.md - the combined sticky-comment body
# labels.txt - deduped labels, one per line (may be empty)
# decision.txt - "pass" or "fail"
# success_count.txt - number of reviewers that produced a usable review
#
# Logs ::group:: sections and debug output to stderr (kept off stdout so the
# output files are the only contract). Exit status is 0 even when every
# reviewer failed — the caller inspects success_count.txt and decides whether
# to fail the workflow. A non-zero exit means a usage error only.
set -euo pipefail
WORK_DIR="${1:-}"
if [ -z "$WORK_DIR" ] || [ ! -d "$WORK_DIR" ]; then
echo "usage: $0 <work_dir> <model> [<model> ...]" >&2
exit 2
fi
shift
MODELS=("$@")
if [ "${#MODELS[@]}" -eq 0 ]; then
echo "error: at least one model name is required" >&2
exit 2
fi
DEBUG_MODE="${DEBUG_MODE:-false}"
HEAD_SHA="${HEAD_SHA:-}"
COMBINED_REVIEW=""
ALL_LABELS=""
ANY_FAIL="false"
SUCCESS_COUNT=0
# Assemble the combined comment in model order. Reviewers are anonymized as
# "Reviewer N" — the model -> number mapping appears only in the (debug) logs
# below, so readers judge the feedback, not the model.
for i in "${!MODELS[@]}"; do
REVIEWER_NUM=$((i + 1))
EXIT_CODE="$(cat "$WORK_DIR/code_$i" 2>/dev/null || echo 1)"
# A missing/empty/garbage exit-code file counts as a failed reviewer rather
# than tripping the numeric comparison below (which would print "integer
# expression expected" and then fall through to wrongly treating the reviewer
# as a success).
[[ "$EXIT_CODE" =~ ^[0-9]+$ ]] || EXIT_CODE=1
AI_RESPONSE="$(cat "$WORK_DIR/resp_$i.json" 2>/dev/null || echo "")"
echo "::group::Reviewer #$REVIEWER_NUM (${MODELS[i]})" >&2
if [ "$DEBUG_MODE" = "true" ]; then
{
echo "=== RAW AI RESPONSE (reviewer #$REVIEWER_NUM, exit $EXIT_CODE) ==="
echo "$AI_RESPONSE"
echo "--- stderr ---"
cat "$WORK_DIR/err_$i.log" 2>/dev/null || true
echo "=== END RAW AI RESPONSE ==="
} >&2
fi
# One model failing (hard error, empty, or non-JSON) must not sink the others:
# record a short failure note and keep going. The caller fails the workflow
# only if *every* reviewer failed (success_count == 0). A reviewer that errors
# out never sets a verdict, so it can never flip the aggregate decision to
# "fail" — only a model that returns a valid "fail" verdict does. Sensitive raw
# output is never written to the comment.
# Require a JSON *object*: a bare `jq .` accepts any valid JSON, including a
# top-level string/array/number that a refusing or misbehaving model might
# emit ("I refuse to review this"). That would pass the gate and then crash
# the `.review` access below under `set -e`, sinking *every* reviewer and
# writing no output at all. `type == "object"` rejects non-objects so they
# degrade to a per-reviewer failure note, preserving the others.
if [ "$EXIT_CODE" -ne 0 ] || [ -z "$AI_RESPONSE" ] || ! echo "$AI_RESPONSE" | jq -e 'type == "object"' >/dev/null 2>&1; then
echo "⚠️ Reviewer #$REVIEWER_NUM did not return a usable review (exit $EXIT_CODE)" >&2
REVIEW_BODY="_This reviewer could not complete its review (see workflow logs for details)._"
LABELS=""
else
REVIEW_RAW=$(echo "$AI_RESPONSE" | jq -r '.review // "No review provided"')
# Trim whitespace with tr (the verdict is a single token). The previous
# `| xargs` aborted the entire script on an unbalanced quote in the value,
# which—inside this per-reviewer loop—would sink every reviewer.
DECISION=$(echo "$AI_RESPONSE" | jq -r '.fail_pass_workflow // "uncertain"' | tr -d '[:space:]')
LABELS=$(echo "$AI_RESPONSE" | jq -r '.labels_added[]? // empty')
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
[ "$DECISION" = "fail" ] && ANY_FAIL="true"
# Each review opens with a "## AI Code Review" H2 and closes with the
# singular Friendly AI Reviewer footer. Under a "### 👤 Reviewer N"
# subheading the H2 is redundant and the footer would repeat once per model,
# so strip both here; a single (plural) footer is added to the combined
# comment below. The footer match keys on the stable "Review by [Friendly AI
# Reviewer]" prefix and the \s*\z tail absorbs trailing whitespace; if the
# upstream markers ever change and aren't found, the text is left intact
# (nothing is dropped).
REVIEW_BODY=$(printf '%s' "$REVIEW_RAW" \
| perl -0pe 's/\A\s*##\s*AI Code Review\s*\n+//' \
| perl -0pe 's/\n*---\s*\n\*Review by \[Friendly AI Reviewer\][^\n]*\n?\s*\z//s')
fi
# Lead each section with a blank line so the "### Reviewer N" heading always
# renders (command substitution strips trailing newlines, so the separator
# must precede the heading, not follow the previous body).
COMBINED_REVIEW=$(printf '%s\n\n### 👤 Reviewer %s\n\n%s' "$COMBINED_REVIEW" "$REVIEWER_NUM" "$REVIEW_BODY")
[ -n "$LABELS" ] && ALL_LABELS=$(printf '%s\n%s' "$ALL_LABELS" "$LABELS")
echo "::endgroup::" >&2
done
# Aggregate labels (union across reviewers) and the pass/fail decision (request
# changes if ANY reviewer did).
LABELS=$(printf '%s\n' "$ALL_LABELS" | sed '/^[[:space:]]*$/d' | sort -u)
if [ "$ANY_FAIL" = "true" ]; then
DECISION="fail"
else
DECISION="pass"
fi
REVIEWER_WORD="reviewers"
[ "${#MODELS[@]}" -eq 1 ] && REVIEWER_WORD="reviewer"
STICKY_MARKER="<!-- ai-code-review:sticky -->"
FOOTER="*Reviews by [Friendly AI Reviewer](https://github.com/LearningCircuit/Friendly-AI-Reviewer) - made with ❤️*"
# COMBINED_REVIEW already starts with "\n\n", so it follows the title directly
# (no extra newline) to yield exactly one blank line before "### Reviewer 1".
# The backticks in `%s` are intentional literal Markdown (inline-code the SHA),
# not a command substitution — single quotes keep them literal on purpose.
# shellcheck disable=SC2016
COMMENT_BODY=$(printf '%s\n\n## 🤖 AI Code Review (%s %s)%s\n\n---\n%s\n\n_Last reviewed at commit `%s`_' \
"$STICKY_MARKER" "${#MODELS[@]}" "$REVIEWER_WORD" "$COMBINED_REVIEW" "$FOOTER" "${HEAD_SHA:0:7}")
printf '%s' "$COMMENT_BODY" > "$WORK_DIR/comment_body.md"
printf '%s' "$LABELS" > "$WORK_DIR/labels.txt"
printf '%s' "$DECISION" > "$WORK_DIR/decision.txt"
printf '%s' "$SUCCESS_COUNT" > "$WORK_DIR/success_count.txt"
+594
View File
@@ -0,0 +1,594 @@
#!/bin/bash
# File Whitelist Security Check Script
# Enhanced security checks with comprehensive file type detection
set -euo pipefail
# Load allowed file patterns from shared whitelist (single source of truth)
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
WHITELIST_FILE="$REPO_ROOT/.file-whitelist.txt"
if [ ! -f "$WHITELIST_FILE" ]; then
echo "❌ Missing .file-whitelist.txt — cannot run whitelist check."
exit 1
fi
ALLOWED_PATTERNS=()
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
ALLOWED_PATTERNS+=("$line")
done < "$WHITELIST_FILE"
# Load per-check ignore lists (exact paths to skip for specific checks)
IGNORE_ENV_FILES=()
IGNORE_ENV_FILE="$REPO_ROOT/.github/security/ignore-env-files.txt"
if [ -f "$IGNORE_ENV_FILE" ]; then
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
IGNORE_ENV_FILES+=("$line")
done < "$IGNORE_ENV_FILE"
fi
IGNORE_SUSPICIOUS_FILETYPES=()
IGNORE_SUSPICIOUS_FILE="$REPO_ROOT/.github/security/ignore-suspicious-filetypes.txt"
if [ -f "$IGNORE_SUSPICIOUS_FILE" ]; then
while IFS= read -r line; do
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
IGNORE_SUSPICIOUS_FILETYPES+=("$line")
done < "$IGNORE_SUSPICIOUS_FILE"
fi
# Get list of files to check
if [ "${CHECK_ALL_FILES:-}" = "true" ]; then
echo "🔍 Checking ALL tracked files (release gate mode)..."
CHANGED_FILES=$(git ls-files)
TOTAL_FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
echo "📋 Found $TOTAL_FILE_COUNT tracked files to check"
elif [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
# For PRs: check all files that would be added/modified in the entire PR
echo "🔍 Checking files in PR from $GITHUB_BASE_REF to HEAD..."
CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/"$GITHUB_BASE_REF"..HEAD)
FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
echo "📋 Found $FILE_COUNT changed files with git diff"
# Also get newly added files across all commits in the PR
# Use a more robust approach that handles edge cases
ALL_NEW_FILES=$(git log --name-only --pretty=format: --diff-filter=A origin/"$GITHUB_BASE_REF"..HEAD 2>/dev/null | grep -v '^$' | sort | uniq || echo "")
NEW_FILE_COUNT=$(echo "$ALL_NEW_FILES" | wc -w)
echo "📋 Found $NEW_FILE_COUNT newly added files with git log"
# Combine both lists and remove duplicates - handle empty ALL_NEW_FILES
if [ -n "$ALL_NEW_FILES" ]; then
CHANGED_FILES=$(echo -e "$CHANGED_FILES\n$ALL_NEW_FILES" | sort | uniq | grep -v '^$')
fi
TOTAL_FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
echo "📋 Total unique files to check: $TOTAL_FILE_COUNT"
else
# For direct pushes: check files in the current commit
echo "🔍 Checking files in latest commit..."
CHANGED_FILES=$(git diff --name-only --diff-filter=AM HEAD~1..HEAD)
TOTAL_FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
echo "📋 Found $TOTAL_FILE_COUNT files in direct push"
fi
echo "🔍 Running comprehensive security checks..."
echo ""
FILES_CHECKED=0
WHITELIST_VIOLATIONS=()
LARGE_FILES=()
BINARY_FILES=()
SUSPICIOUS_FILES=()
RESEARCH_DATA_VIOLATIONS=()
FLASK_SECRET_VIOLATIONS=()
ENV_FILE_VIOLATIONS=()
HIGH_ENTROPY_VIOLATIONS=()
HARDCODED_PATH_VIOLATIONS=()
HARDCODED_IP_VIOLATIONS=()
SUSPICIOUS_FILETYPE_VIOLATIONS=()
# Use improved file processing that handles spaces and special characters
while IFS= read -r file; do
[ -z "$file" ] && continue
# Skip deleted files
if [ ! -f "$file" ]; then
continue
fi
FILES_CHECKED=$((FILES_CHECKED + 1))
if [ $((FILES_CHECKED % 10)) -eq 0 ]; then
printf "."
fi
# 1. Whitelist check
ALLOWED=false
for pattern in "${ALLOWED_PATTERNS[@]}"; do
if echo "$file" | grep -qE "$pattern"; then
ALLOWED=true
break
fi
done
if [ "$ALLOWED" = "false" ]; then
WHITELIST_VIOLATIONS+=("$file")
fi
# 2. Large file check (>1MB)
if [ -f "$file" ]; then
FILE_SIZE=$(stat -c%s "$file" 2>/dev/null || echo 0)
if [ "$FILE_SIZE" -gt 1048576 ]; then
LARGE_FILES+=("$file ($(echo "$FILE_SIZE" | awk '{printf "%.1fMB", $1/1024/1024}'))")
fi
fi
# 3. Binary file check
if file "$file" | grep -q "binary"; then
BINARY_FILES+=("$file")
fi
# 4. Secret pattern check - REMOVED: gitleaks workflow handles this more accurately
# 5. Suspicious filename patterns - whitelist approach
SAFE_FILENAME_PATTERNS=(
".*token_counter.*\.py$"
".*migrate.*token.*\.py$"
".*enhanced.*token.*\.md$"
"docs/.*token.*\.md$"
"tests/.*\.py$"
"docs/decisions/.*\.md$"
".*session_passwords\.py$"
".*change_password\.html$"
"tests/ui_tests/.*password.*\.js$"
".*password_validator\.py$"
".*password_utils\.py$"
)
# Check if filename looks suspicious
if echo "$file" | grep -iE "(secret|password|token|\.key$|\.pem$|\.p12$|\.pfx$|\.env$)" >/dev/null; then
# Check if filename matches whitelist patterns
FILENAME_WHITELISTED=false
for pattern in "${SAFE_FILENAME_PATTERNS[@]}"; do
if echo "$file" | grep -qE "$pattern"; then
FILENAME_WHITELISTED=true
break
fi
done
if [ "$FILENAME_WHITELISTED" = "false" ]; then
SUSPICIOUS_FILES+=("$file")
fi
fi
# 6. LDR-specific security checks
# Check for research data leakage
if [ -f "$file" ] && [ -r "$file" ]; then
# Check for hardcoded research queries in non-test files
if ! echo "$file" | grep -qE "(test|mock|example)"; then
if grep -E "(research_id|session_id|query_id).*=.*[\"'][0-9a-f]{8,}[\"']" "$file" >/dev/null 2>&1; then
RESEARCH_DATA_VIOLATIONS+=("$file")
fi
fi
# Check for Flask secret keys
if grep -E "SECRET_KEY.*=.*[\"'][^\"']{16,}[\"']" "$file" >/dev/null 2>&1; then
if ! grep -iE "(os\.environ|getenv|config\[|example|placeholder)" "$file" >/dev/null 2>&1; then
FLASK_SECRET_VIOLATIONS+=("$file")
fi
fi
# Check for environment files
if echo "$file" | grep -E "\.(env|env\.[a-zA-Z]+)$" >/dev/null; then
ENV_IGNORED=false
for epath in "${IGNORE_ENV_FILES[@]+${IGNORE_ENV_FILES[@]}}"; do
[ "$file" = "$epath" ] && ENV_IGNORED=true && break
done
if [ "$ENV_IGNORED" = "false" ]; then
ENV_FILE_VIOLATIONS+=("$file")
fi
fi
# Check for high-entropy strings (potential keys/secrets)
if [ -f "$file" ] && [ -r "$file" ]; then
# Skip HTML files and other safe file types for entropy checks
if ! echo "$file" | grep -qE "\.(html|css|js|json|yml|yaml|md)$"; then
# Skip news_strategy.py which contains example categories in prompts
if ! echo "$file" | grep -qE "news_strategy\.py$"; then
# Look for base64-like strings or hex strings that are suspiciously long
if grep -E "[a-zA-Z0-9+/]{40,}={0,2}|[a-f0-9]{40,}" "$file" >/dev/null 2>&1; then
# Exclude common false positives
if ! grep -iE "(sha256|md5|hash|test|example|fixture|integrity)" "$file" >/dev/null 2>&1; then
HIGH_ENTROPY_VIOLATIONS+=("$file")
fi
fi
fi
fi
fi
# Check for hardcoded paths (Unix/Windows)
if ! echo "$file" | grep -qE "(test|mock|example|\.md$|docker|Docker|\.yml$|\.yaml$|config/paths\.py$|security/path_validator\.py$)"; then
# Look for absolute paths and user home directories
if grep -E "(/home/[a-zA-Z0-9_-]+|/Users/[a-zA-Z0-9_-]+|C:\\\\Users\\\\[a-zA-Z0-9_-]+|/opt/|/var/|/etc/|/usr/local/)" "$file" >/dev/null 2>&1; then
# Exclude common false positives and Docker volume mounts
if ! grep -iE "(example|sample|placeholder|TODO|FIXME|/usr/local/bin|/etc/hosts|documentation|/etc/searxng|volumes?:|docker)" "$file" >/dev/null 2>&1; then
HARDCODED_PATH_VIOLATIONS+=("$file")
fi
fi
fi
# Check for hardcoded IP addresses
if ! echo "$file" | grep -qE "(test|mock|example|\.md$)"; then
# Look for IPv4 addresses (excluding common safe ones)
if grep -E "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" "$file" >/dev/null 2>&1; then
# Exclude localhost, documentation IPs, and common examples
if ! grep -E "\b(127\.0\.0\.1|0\.0\.0\.0|localhost|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|255\.255\.255\.|192\.0\.2\.|198\.51\.100\.|203\.0\.113\.)" "$file" >/dev/null 2>&1; then
# Additional check to exclude obvious non-IPs (version numbers, etc)
if grep -E "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" "$file" | grep -vE "(version|v[0-9]+\.|release|tag)" >/dev/null 2>&1; then
HARDCODED_IP_VIOLATIONS+=("$file")
fi
fi
fi
fi
# 7. Suspicious file type check - detect potentially dangerous file types
if [ -f "$file" ]; then
# Check if file is in the suspicious-filetypes ignore list
FILETYPE_IGNORED=false
for fpath in "${IGNORE_SUSPICIOUS_FILETYPES[@]+${IGNORE_SUSPICIOUS_FILETYPES[@]}}"; do
[ "$file" = "$fpath" ] && FILETYPE_IGNORED=true && break
done
if [ "$FILETYPE_IGNORED" = "false" ]; then
# Check for suspicious file extensions
if echo "$file" | grep -iE "\.(exe|dll|so|dylib|bin|deb|rpm|msi|dmg|pkg|app)$" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (executable/binary)")
elif echo "$file" | grep -iE "\.(zip|tar|gz|rar|7z|tar\.gz|tar\.bz2|tgz)$" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (compressed archive)")
elif echo "$file" | grep -iE "\.(log|tmp|temp|cache|bak|backup|swp|swo|DS_Store|thumbs\.db|desktop\.ini|~|\.orig|\.rej|\.patch)$" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (temporary/cache)")
elif echo "$file" | grep -iE "\.(png|jpg|jpeg|gif|bmp|tiff|svg|ico|webp)$" >/dev/null; then
# Images are suspicious unless in specific directories
if ! echo "$file" | grep -qE "(^docs/images/|^src/local_deep_research/web/static/favicon\.png$|^installers/.*\.ico$)"; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (image file)")
fi
elif echo "$file" | grep -iE "\.(mp3|mp4|wav|avi|mov|mkv|flv|wmv|webm|m4a|ogg)$" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (media file)")
elif echo "$file" | grep -iE "\.(csv|xlsx|xls|doc|docx|pdf|ppt|pptx)$" >/dev/null; then
# Documents are suspicious unless in docs directory
if ! echo "$file" | grep -qE "docs/"; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (document file)")
fi
elif echo "$file" | grep -iE "\.(db|sqlite|sqlite3)$" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (database file)")
elif echo "$file" | grep -iE "node_modules/|__pycache__/|\.pyc$|\.pyo$|\.egg-info/|dist/|build/|\.cache/" >/dev/null; then
SUSPICIOUS_FILETYPE_VIOLATIONS+=("$file (build artifact/cache)")
fi
fi
fi
fi
done <<< "$CHANGED_FILES"
echo ""
echo "✓ Checked $FILES_CHECKED files"
echo ""
# Report all violations with detailed explanations
echo "📊 Security scan completed. Analyzing results..."
echo "📋 Summary of findings:"
echo " - File type violations: ${#WHITELIST_VIOLATIONS[@]}"
echo " - Large files: ${#LARGE_FILES[@]}"
echo " - Binary files: ${#BINARY_FILES[@]}"
echo " - Suspicious filenames: ${#SUSPICIOUS_FILES[@]}"
echo " - Research data leaks: ${#RESEARCH_DATA_VIOLATIONS[@]}"
echo " - Hardcoded Flask secrets: ${#FLASK_SECRET_VIOLATIONS[@]}"
echo " - Environment files: ${#ENV_FILE_VIOLATIONS[@]}"
echo " - High-entropy strings: ${#HIGH_ENTROPY_VIOLATIONS[@]}"
echo " - Hardcoded paths: ${#HARDCODED_PATH_VIOLATIONS[@]}"
echo " - Hardcoded IPs: ${#HARDCODED_IP_VIOLATIONS[@]}"
echo " - Suspicious file types: ${#SUSPICIOUS_FILETYPE_VIOLATIONS[@]}"
TOTAL_VIOLATIONS=0
if [ ${#WHITELIST_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ WHITELIST VIOLATIONS - File types not allowed in repository:"
echo " These files don't match any pattern in .file-whitelist.txt."
echo " Binary files (images, audio, etc.) bloat the repo and should NOT be committed."
echo " Only a small set of explicitly listed binary files is allowed — store others externally."
echo " If this is a legitimate text/config file, add it to .file-whitelist.txt (requires maintainer approval)."
echo ""
for violation in "${WHITELIST_VIOLATIONS[@]}"; do
echo " 🚫 $violation"
# Show file type and extension
FILE_EXT="${violation##*.}"
if [ -f "$violation" ]; then
FILE_TYPE=$(file -b "$violation" 2>/dev/null || echo "unknown")
echo " → File extension: .$FILE_EXT"
echo " → File type: $FILE_TYPE"
echo " → First few lines:"
head -3 "$violation" 2>/dev/null | while read -r line; do
echo " $line"
done
fi
echo " → Issue: File extension/type not in .file-whitelist.txt"
echo " → Fix: For text/config files, add pattern to .file-whitelist.txt"
echo " → Note: Binary files should NOT be added to the repo — store them externally"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#WHITELIST_VIOLATIONS[@]}))
fi
if [ ${#LARGE_FILES[@]} -gt 0 ]; then
echo ""
echo "❌ LARGE FILES (>1MB) - Files too big for repository:"
echo " Large files should typically be stored externally or compressed."
echo ""
for violation in "${LARGE_FILES[@]}"; do
echo " 📏 $violation"
echo " → Issue: File size exceeds 1MB limit"
echo " → Fix: Use Git LFS, external storage, or compress the file"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#LARGE_FILES[@]}))
fi
if [ ${#BINARY_FILES[@]} -gt 0 ]; then
echo ""
echo "⚠️ BINARY FILES DETECTED - Review these carefully:"
echo " Binary files may contain sensitive data and can't be easily reviewed."
echo ""
for violation in "${BINARY_FILES[@]}"; do
echo " 🔒 $violation"
echo " → Issue: Binary file detected (contents not reviewable)"
echo " → Action: Verify this file doesn't contain sensitive data"
echo ""
done
fi
if [ ${#SUSPICIOUS_FILES[@]} -gt 0 ]; then
echo ""
echo "❌ SUSPICIOUS FILENAMES - Files with security-sensitive names:"
echo " These filenames contain words that often indicate sensitive files."
echo ""
for violation in "${SUSPICIOUS_FILES[@]}"; do
echo " 🚨 $violation"
# Show which keyword triggered the detection
if echo "$violation" | grep -qi "secret"; then
echo " → Triggered by: 'secret' in filename"
elif echo "$violation" | grep -qi "password"; then
echo " → Triggered by: 'password' in filename"
elif echo "$violation" | grep -qi "token"; then
echo " → Triggered by: 'token' in filename"
elif echo "$violation" | grep -qi "api"; then
echo " → Triggered by: 'api' in filename"
elif echo "$violation" | grep -qi "key"; then
echo " → Triggered by: 'key' in filename"
fi
# Show file content preview if it exists
if [ -f "$violation" ]; then
FILE_TYPE=$(file -b "$violation" 2>/dev/null || echo "unknown")
FILE_SIZE=$(stat -c%s "$violation" 2>/dev/null || echo "unknown")
echo " → File info: $FILE_TYPE (${FILE_SIZE} bytes)"
echo " → Content preview:"
head -3 "$violation" 2>/dev/null | while read -r line; do
echo " $line"
done
fi
echo " → Issue: Filename contains suspicious keywords (secret/password/token/key)"
echo " → Fix: Rename file or add to SAFE_FILENAME_PATTERNS whitelist"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#SUSPICIOUS_FILES[@]}))
fi
# LDR-specific violation reports
if [ ${#RESEARCH_DATA_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ RESEARCH DATA LEAKAGE - Hardcoded research session data found:"
echo " Research IDs and session data should never be hardcoded in production code."
echo ""
for violation in "${RESEARCH_DATA_VIOLATIONS[@]}"; do
echo " 📊 $violation"
# Show the specific lines with research data
echo " → Found hardcoded research data:"
grep -n -E "(research_id|session_id|query_id).*=.*[\"'][0-9a-f]{8,}[\"']" "$violation" 2>/dev/null | head -3 | while read -r line; do
echo " $line"
done
echo " → Issue: Hardcoded research/session IDs in non-test file"
echo " → Fix: Use environment variables or configuration files"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#RESEARCH_DATA_VIOLATIONS[@]}))
fi
if [ ${#FLASK_SECRET_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ FLASK SECRET KEY - Hardcoded Flask secret keys found:"
echo " Flask secret keys must never be hardcoded for security reasons."
echo ""
for violation in "${FLASK_SECRET_VIOLATIONS[@]}"; do
echo " 🔐 $violation"
# Show the specific lines with secret keys
echo " → Found hardcoded Flask secret key:"
grep -n -E "SECRET_KEY.*=.*[\"'][^\"']{16,}[\"']" "$violation" 2>/dev/null | head -3 | while read -r line; do
echo " $line"
done
echo " → Issue: Hardcoded Flask SECRET_KEY"
echo " → Fix: Use os.environ or load from secure config file"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#FLASK_SECRET_VIOLATIONS[@]}))
fi
if [ ${#ENV_FILE_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ ENVIRONMENT FILES - .env files detected:"
echo " Environment files contain sensitive configuration and should never be committed."
echo ""
for violation in "${ENV_FILE_VIOLATIONS[@]}"; do
echo " 🌍 $violation"
echo " → Issue: Environment file in repository"
echo " → Fix: Add to .gitignore and use .env.example instead"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#ENV_FILE_VIOLATIONS[@]}))
fi
if [ ${#HIGH_ENTROPY_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ HIGH ENTROPY STRINGS - Potential secrets or keys detected:"
echo " Long random strings may be API keys, tokens, or other secrets."
echo ""
for violation in "${HIGH_ENTROPY_VIOLATIONS[@]}"; do
echo " 🎲 $violation"
# Show sample of high entropy strings
echo " → Found high-entropy strings:"
grep -n -E "[a-zA-Z0-9+/]{40,}={0,2}|[a-f0-9]{40,}" "$violation" 2>/dev/null | head -3 | while read -r line; do
# Truncate long lines for readability
echo " ${line:0:120}..."
done
echo " → Issue: High-entropy strings that could be secrets"
echo " → Fix: Review and move to environment variables if sensitive"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#HIGH_ENTROPY_VIOLATIONS[@]}))
fi
if [ ${#HARDCODED_PATH_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ HARDCODED PATHS - System-specific paths detected:"
echo " Absolute paths can expose system structure and break portability."
echo ""
for violation in "${HARDCODED_PATH_VIOLATIONS[@]}"; do
echo " 📁 $violation"
# Show the specific hardcoded paths
echo " → Found hardcoded paths:"
grep -n -E "(/home/[a-zA-Z0-9_-]+|/Users/[a-zA-Z0-9_-]+|C:\\\\Users\\\\[a-zA-Z0-9_-]+|/opt/|/var/|/etc/|/usr/local/)" "$violation" 2>/dev/null | head -5 | while read -r line; do
echo " $line"
done
echo " → Issue: Hardcoded absolute paths reduce portability"
echo " → Fix: Use relative paths, environment variables, or config files"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#HARDCODED_PATH_VIOLATIONS[@]}))
fi
if [ ${#HARDCODED_IP_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ HARDCODED IP ADDRESSES - External IP addresses detected:"
echo " Hardcoded IPs can expose infrastructure and cause connectivity issues."
echo ""
for violation in "${HARDCODED_IP_VIOLATIONS[@]}"; do
echo " 🌐 $violation"
# Show the specific IP addresses
echo " → Found hardcoded IP addresses:"
grep -n -E "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" "$violation" 2>/dev/null | grep -v -E "(127\.0\.0\.1|0\.0\.0\.0|localhost|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|255\.255\.255\.|192\.0\.2\.|198\.51\.100\.|203\.0\.113\.)" | head -5 | while read -r line; do
echo " $line"
done
echo " → Issue: Hardcoded IP addresses (non-private/localhost)"
echo " → Fix: Use DNS names, environment variables, or config files"
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#HARDCODED_IP_VIOLATIONS[@]}))
fi
if [ ${#SUSPICIOUS_FILETYPE_VIOLATIONS[@]} -gt 0 ]; then
echo ""
echo "❌ SUSPICIOUS FILE TYPES - Potentially dangerous file types detected:"
echo " These file types are commonly used for malware, data leaks, or bloat the repository."
echo ""
for violation in "${SUSPICIOUS_FILETYPE_VIOLATIONS[@]}"; do
echo " 🚨 $violation"
FILE_PATH="${violation%% (*}"
FILE_CATEGORY="${violation##*\\(}"
FILE_CATEGORY="${FILE_CATEGORY%\\)}"
# Provide specific guidance based on file category
case "$FILE_CATEGORY" in
"executable/binary")
echo " → Issue: Executable/binary files can contain malware"
echo " → Fix: Remove executable files, use package managers instead"
;;
"compressed archive")
echo " → Issue: Compressed archives hide their contents from review"
echo " → Fix: Extract contents and commit individual files instead"
;;
"temporary/cache")
echo " → Issue: Temporary/cache files should not be committed"
echo " → Fix: Add to .gitignore and remove from repository"
;;
"image file")
echo " → Issue: Binary image files bloat the repo and should NOT be committed"
echo " → Fix: Store images externally. Only a few explicitly listed images in docs/images/ are allowed"
;;
"media file")
echo " → Issue: Media files are large and rarely needed in code repos"
echo " → Fix: Use external hosting or remove if unnecessary"
;;
"document file")
echo " → Issue: Office documents should be in docs/ directory if needed"
echo " → Fix: Move to docs/ directory or convert to markdown"
;;
"database file")
echo " → Issue: Database files contain data that shouldn't be in source control"
echo " → Fix: Add to .gitignore and use migrations/seeds instead"
;;
"build artifact/cache")
echo " → Issue: Build artifacts and cache files bloat the repository"
echo " → Fix: Add to .gitignore and remove from repository"
;;
esac
# Show file info if available
if [ -f "$FILE_PATH" ]; then
FILE_SIZE=$(stat -c%s "$FILE_PATH" 2>/dev/null || echo "unknown")
if [ "$FILE_SIZE" != "unknown" ]; then
READABLE_SIZE=$(echo "$FILE_SIZE" | awk '{if($1>=1048576) printf "%.1fMB", $1/1048576; else if($1>=1024) printf "%.1fKB", $1/1024; else printf "%dB", $1}')
echo " → File size: $READABLE_SIZE"
fi
fi
echo ""
done
TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + ${#SUSPICIOUS_FILETYPE_VIOLATIONS[@]}))
fi
# Final result
if [ $TOTAL_VIOLATIONS -eq 0 ]; then
echo ""
echo "✅ All security checks passed!"
exit 0
else
echo ""
echo "💡 To fix these issues:"
echo " - For text/config files: add pattern to .file-whitelist.txt (requires maintainer approval)"
echo " - For binary files (images, audio, video, archives): do NOT add to the repo"
echo " Binary files permanently bloat git history. Store them externally instead."
echo " Only a small set of explicitly listed binary files is permitted."
echo " - Use environment variables for secrets"
echo " - Never hardcode research data or session IDs"
echo " - Use .env.example files instead of .env"
echo " - Replace absolute paths with relative paths or configs"
echo " - Use DNS names instead of hardcoded IP addresses"
echo ""
echo "⚠️ SECURITY REMINDER: This is a public repository!"
echo " Never commit sensitive data, API keys, or personal information."
exit 1
fi
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Filter ``npm audit --json`` output against an allowlist of advisories.
Reads ``npm audit --json`` on stdin. Exits non-zero (1) if any
moderate-or-higher vulnerability is NOT fully explained by allowlisted
advisories, otherwise 0.
npm reports a parent package as vulnerable when it (transitively) depends on
a vulnerable child; the parent's ``via`` then contains a *string* reference to
the child rather than an advisory object. We resolve those ``via`` chains so a
package that is vulnerable *only* because of an allowlisted advisory is itself
treated as handled — otherwise allowlisting one leaf (e.g. js-yaml) would still
leave its dozen parents (lhci/*, jest/*) failing the gate.
The allowlist is supplied via the ``AUDIT_ALLOWLIST`` env var as a
space-separated list of GHSA IDs. Keep it tight and justified in the workflow
that sets it — only advisories with **no available fix** in **dev/test-only**
tooling belong here.
"""
import json
import os
import sys
ALLOW = set(os.environ.get("AUDIT_ALLOWLIST", "").split())
SEVERITIES = {"moderate", "high", "critical"}
def main() -> int:
raw = sys.stdin.read().strip()
if not raw:
print(
"::error::empty npm audit output (audit did not run?)",
file=sys.stderr,
)
return 1
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
print(
f"::error::could not parse npm audit JSON: {exc}", file=sys.stderr
)
return 1
# A genuine npm audit report always carries these keys (an empty/clean
# audit still has "vulnerabilities": {}). Their absence means the audit
# did NOT actually run — e.g. a registry/network error returns valid JSON
# like {"message": ..., "error": ...} and npm exits non-zero, which the
# caller's `|| true` swallows. Fail safe (gate red) rather than pass a
# green gate on an audit that never happened.
if "vulnerabilities" not in data or "auditReportVersion" not in data:
print(
"::error::npm audit did not return a valid report (audit failed "
f"to run?): {data.get('message', 'unknown error')}",
file=sys.stderr,
)
return 1
vulns = data.get("vulnerabilities") or {}
if not isinstance(vulns, dict):
print(
"::error::npm audit 'vulnerabilities' is not an object — "
"treating as a failed audit",
file=sys.stderr,
)
return 1
def reachable_ghsas(name, seen):
"""All advisory GHSAs reachable from a package's via-chain."""
if name in seen:
return set()
seen.add(name)
found = set()
for via in (vulns.get(name) or {}).get("via", []):
if isinstance(via, dict) and "GHSA-" in (via.get("url") or ""):
# Take only the GHSA token, not any trailing path/query the
# URL might carry (e.g. .../GHSA-xxxx/foo?bar).
tail = via["url"].split("GHSA-", 1)[1]
found.add("GHSA-" + tail.split("/")[0].split("?")[0])
elif isinstance(via, str):
found |= reachable_ghsas(via, seen)
return found
unhandled = 0
for name, info in vulns.items():
if info.get("severity") not in SEVERITIES:
continue
ghsas = reachable_ghsas(name, set())
if ghsas and ghsas <= ALLOW:
print(f" allowlisted: {name} ({info['severity']}) {sorted(ghsas)}")
else:
unhandled += 1
detail = sorted(ghsas - ALLOW) if ghsas else "no-GHSA"
print(f" UNHANDLED: {name} ({info['severity']}) {detail}")
suffix = "y" if unhandled == 1 else "ies"
print(f" -> {unhandled} non-allowlisted moderate+ vulnerabilit{suffix}")
return 1 if unhandled else 0
if __name__ == "__main__":
sys.exit(main())
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
# Validates that all docker-compose image references use SHA256 digests
# Prevents supply chain attacks by ensuring immutable image references
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration - images that are allowed without SHA digests
ALLOWED_EXCEPTIONS=(
"localdeepresearch/local-deep-research:latest" # Own image, built by CI
)
# Check if an image reference is in the exceptions list
is_exception() {
local image="$1"
for exception in "${ALLOWED_EXCEPTIONS[@]}"; do
if [[ "$image" == "$exception" ]]; then
return 0
fi
done
return 1
}
# Validate a single docker-compose file
validate_compose_file() {
local file="$1"
local violations=0
local line_num=0
while IFS= read -r line; do
line_num=$((line_num + 1))
# Check if this line contains an image reference
if [[ "$line" =~ ^[[:space:]]*image:[[:space:]]*(.+)$ ]]; then
local image="${BASH_REMATCH[1]}"
image=$(echo "$image" | tr -d '"' | xargs) # Remove quotes and whitespace
# Skip if it's an exception
if is_exception "$image"; then
echo -e "${YELLOW} Line $line_num: $image (exception)${NC}"
continue
fi
# Check if image has SHA digest
if [[ ! "$image" =~ @sha256: ]]; then
echo -e "${RED} ❌ Line $line_num: Missing SHA digest${NC}"
echo -e "${RED} Image: $image${NC}"
violations=$((violations + 1))
else
echo -e "${GREEN} ✓ Line $line_num: $image${NC}"
fi
fi
done < "$file"
return $violations
}
# Main validation logic
main() {
local total_violations=0
local files_checked=0
echo "🔍 Validating docker-compose image pinning..."
echo ""
# Find all docker-compose files
while IFS= read -r compose_file; do
# Skip cookiecutter templates and examples (documentation only)
if [[ "$compose_file" =~ cookiecutter-docker/ ]] || [[ "$compose_file" =~ examples/ ]]; then
echo -e "${YELLOW}⏭ Skipping: $compose_file (template/example)${NC}"
continue
fi
echo "📄 Checking: $compose_file"
if validate_compose_file "$compose_file"; then
: # No violations
else
violations=$?
total_violations=$((total_violations + violations))
fi
files_checked=$((files_checked + 1))
echo ""
done < <(find . -name "docker-compose*.yml" -o -name "docker-compose*.yaml")
# Summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Summary:"
echo " Files checked: $files_checked"
echo " Violations: $total_violations"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ $total_violations -gt 0 ]; then
echo ""
echo -e "${RED}❌ Found $total_violations unpinned images in docker-compose files${NC}"
echo ""
echo "Images must use SHA256 digests for security and reproducibility."
echo ""
echo "To fix:"
echo " 1. Pull the image: docker pull <image:tag>"
echo " 2. Get digest: docker inspect <image:tag> | jq -r '.[0].RepoDigests[0]'"
echo " 3. Update file: image: <image:tag>@sha256:..."
echo ""
echo "Example:"
echo " # Bad"
echo " image: ollama/ollama:latest"
echo ""
echo " # Good"
echo " image: ollama/ollama:latest@sha256:8850b8b33936b9fb..."
exit 1
fi
echo ""
echo -e "${GREEN}✅ All docker-compose images properly pinned${NC}"
exit 0
}
main "$@"
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Validates that all GitHub Actions workflow service containers and container images
use SHA256 digests for supply chain security.
Prevents tag tampering attacks by ensuring immutable image references.
"""
import sys
from pathlib import Path
from typing import List, Tuple
try:
import yaml
except ImportError:
print("❌ Error: PyYAML is required. Install with: pip install pyyaml")
sys.exit(1)
# ANSI color codes
class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m" # No Color
def has_sha_digest(image_ref: str) -> bool:
"""Check if image reference includes SHA256 digest."""
return "@sha256:" in image_ref
def validate_workflow(workflow_path: Path) -> List[Tuple[str, str, str]]:
"""
Validate a workflow file for unpinned images.
Returns:
List of (job_name, violation_type, image) tuples
"""
violations = []
try:
with open(workflow_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
if not isinstance(data, dict) or "jobs" not in data:
return violations
# Check each job
for job_name, job_def in data["jobs"].items():
if not isinstance(job_def, dict):
continue
# Check container: field
if "container" in job_def:
container = job_def["container"]
# Container can be a string or dict with 'image' key
if isinstance(container, str):
if not has_sha_digest(container):
violations.append((job_name, "container", container))
elif isinstance(container, dict) and "image" in container:
image = container["image"]
if not has_sha_digest(image):
violations.append((job_name, "container", image))
# Check services: field
if "services" in job_def and isinstance(job_def["services"], dict):
for service_name, service_def in job_def["services"].items():
if isinstance(service_def, dict) and "image" in service_def:
image = service_def["image"]
if not has_sha_digest(image):
violations.append(
(job_name, f"service '{service_name}'", image)
)
except yaml.YAMLError as e:
print(f"{Colors.RED}❌ YAML parse error in {workflow_path}:{Colors.NC}")
print(f" {e}")
# Return a violation to fail the check
violations.append(("parse_error", "error", str(e)))
except Exception as e:
print(
f"{Colors.RED}❌ Error processing {workflow_path}: {e}{Colors.NC}"
)
violations.append(("error", "error", str(e)))
return violations
def main():
"""Main validation logic."""
workflows_dir = Path(".github/workflows")
if not workflows_dir.exists():
print(
f"{Colors.RED}❌ .github/workflows directory not found{Colors.NC}"
)
sys.exit(1)
print("🔍 Validating GitHub Actions workflow images...")
print()
total_violations = 0
files_checked = 0
# Process all workflow files
for workflow_file in sorted(workflows_dir.glob("*.yml")) + sorted(
workflows_dir.glob("*.yaml")
):
violations = validate_workflow(workflow_file)
if violations:
print(f"{Colors.RED}📄 {workflow_file.name}:{Colors.NC}")
for job_name, violation_type, image in violations:
print(
f"{Colors.RED} ❌ Job '{job_name}' {violation_type}: {image}{Colors.NC}"
)
print()
total_violations += len(violations)
else:
print(f"{Colors.GREEN}{workflow_file.name}{Colors.NC}")
files_checked += 1
# Summary
print("" * 50)
print("📊 Summary:")
print(f" Files checked: {files_checked}")
print(f" Violations: {total_violations}")
print("" * 50)
if total_violations > 0:
print()
print(
f"{Colors.RED}❌ Found {total_violations} unpinned images in workflow files{Colors.NC}"
)
print()
print("Service container images must use SHA256 digests for security.")
print()
print("To fix:")
print(" 1. Pull the image: docker pull <image:tag>")
print(
" 2. Get digest: docker inspect <image:tag> | jq -r '.[0].RepoDigests[0]'"
)
print(" 3. Update workflow:")
print()
print("Example:")
print(" services:")
print(" redis:")
print(f"{Colors.RED} image: redis:alpine # Bad{Colors.NC}")
print(
f"{Colors.GREEN} image: redis:alpine@sha256:... # Good{Colors.NC}"
)
return 1
print()
print(f"{Colors.GREEN}✅ All workflow images properly pinned{Colors.NC}")
return 0
if __name__ == "__main__":
sys.exit(main())