mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-04 04:01:07 +00:00
Scorecard's Pinned-Dependencies check requires pip installs to be hash-verified, not just version-pinned - our existing pkg==X.Y.Z pins (and even pip install -r requirements-ci.txt, despite that file already carrying hashes) still scored a 4 because the pin/hash isn't visible on the command line itself. Adds .github/requirements/*.txt: hash-locked files generated via `uv pip compile --generate-hashes` for every pip target that isn't already requirements-ci.txt, covering standalone CI tooling (build, wheel, twine, uv, pip-audit, safety/bandit/semgrep/jq, pip/setuptools bootstrap) and the project's own local-source installs. The latter (`pip install -e ".[explorer]"`, `pip install -e .`) can't be hash-pinned directly since there's nothing to hash for a local source tree; split into `pip install --no-deps -e .` plus a separate hash-pinned install of the actual fetched dependencies instead. Also adds --require-hashes to every `-r requirements-ci.txt` install so hash verification is enforced explicitly rather than only implied by the file's own content. Simplifies the Dockerfile in the process: it now installs from the same pre-generated explorer-extra.txt (copied in at build time) instead of extracting constraints from requirements-ci.txt at build time, which also means setuptools gets its CVE-2025-47273 fix as a side effect of the hash-pinned install rather than a separate upgrade step. benchmark.yml: pip install -r benchmarks/requirements.txt is left unpinned - that directory doesn't exist in this repo, so there's nothing to generate hashes from. Pre-existing breakage, unrelated to this change.
240 lines
10 KiB
YAML
240 lines
10 KiB
YAML
name: Security Scan
|
|
|
|
on:
|
|
schedule:
|
|
- cron: '30 1 * * 1,4' # Mon/Thu 7 AM IST
|
|
push:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- 'mkdocs.yml'
|
|
- 'requirements-docs.txt'
|
|
- '**/*.md'
|
|
pull_request:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- 'docs/**'
|
|
- 'mkdocs.yml'
|
|
- 'requirements-docs.txt'
|
|
- '**/*.md'
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
security-scan:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
security-events: write
|
|
actions: read
|
|
# Needed for the "Comment PR with Security Results" step below. Safe on
|
|
# pull_request (not pull_request_target): GitHub always forces a
|
|
# read-only token for PRs from forks regardless of this permission.
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
|
# Install the pinned dependency set FIRST so Safety scans Semantica's
|
|
# exact CI/release dependency tree (requirements-ci.txt is generated
|
|
# from pyproject.toml extras, so this covers the project's real deps).
|
|
pip install -r requirements-ci.txt --require-hashes
|
|
# Tooling AFTER the pinned set: installing safety/bandit/semgrep/jq
|
|
# first lets the pinned requirements overwrite their transitive deps
|
|
# (e.g. rich), which breaks the safety CLI at runtime.
|
|
pip install -r .github/requirements/security-scan-tools.txt --require-hashes
|
|
|
|
- name: Run Safety Check (Package Vulnerabilities)
|
|
run: |
|
|
# NOTE: Safety 3.x repurposed --output to select a console format
|
|
# (json/text/screen/...), not a file path. Writing JSON to a file
|
|
# now requires --save-json; the previous `--output safety-report.json`
|
|
# usage was silently invalid and never produced a report.
|
|
safety check --save-json safety-report.json || true
|
|
|
|
# Guard 1: fail loudly if Safety exited before writing a report at all
|
|
# (network error, API auth failure, tool crash). Without this check a
|
|
# missing or empty file causes jq to fall back to "0", making a broken
|
|
# scanner indistinguishable from a clean scan.
|
|
if [ ! -s safety-report.json ]; then
|
|
echo "::error::Safety scan produced no report (safety-report.json is missing or empty). Treating as failure — check for network errors, API auth failures, or Safety crashes in the logs above."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Checking for package vulnerabilities..."
|
|
|
|
# No || echo "0" fallback: if jq fails (malformed JSON, missing key,
|
|
# vulnerabilities:null) VULNS will be empty or "null" so guard 2 below
|
|
# catches it rather than silently treating the broken report as zero.
|
|
VULNS=$(jq '.vulnerabilities | length' safety-report.json 2>/dev/null)
|
|
|
|
# Guard 2: ensure VULNS is a non-negative integer before the -gt
|
|
# comparison. "null" (missing/null key) or "" (jq parse failure) would
|
|
# cause bash's -gt to throw an arithmetic error and fall through to the
|
|
# success branch — the same silent-pass bug as a missing file.
|
|
if ! [[ "$VULNS" =~ ^[0-9]+$ ]]; then
|
|
echo "::error::Safety report exists but 'vulnerabilities' is missing or non-numeric (got: '${VULNS}'). The report may be malformed or Safety may have written an error-only JSON. Treating as failure."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$VULNS" -gt 0 ]; then
|
|
echo "❌ Security vulnerabilities found: $VULNS"
|
|
echo "CI will fail to prevent merging of vulnerable dependencies"
|
|
echo ""
|
|
echo "Vulnerability details:"
|
|
jq -r '.vulnerabilities[] | "- \(.package_name)==\(.analyzed_version): \(.vulnerability_id) (\(.CVE // "no CVE assigned"))"' safety-report.json || true
|
|
exit 1
|
|
else
|
|
echo "✅ No security vulnerabilities found"
|
|
fi
|
|
|
|
- name: Run Bandit (Code Security Linter)
|
|
run: |
|
|
bandit -r semantica/ -f json -o bandit-report.json || true
|
|
echo "Checking for HIGH severity security issues..."
|
|
|
|
# Count HIGH severity issues
|
|
HIGH_ISSUES=$(bandit -r semantica/ -f json -ll 2>/dev/null | jq -r '.results[]? | select(.issue_severity == "HIGH") | .test_name' 2>/dev/null | wc -l || echo "0")
|
|
|
|
if [ "$HIGH_ISSUES" -gt 0 ]; then
|
|
echo "❌ HIGH severity security issues found: $HIGH_ISSUES"
|
|
echo "CI will fail to prevent merging of high-risk code"
|
|
echo ""
|
|
echo "High severity issues:"
|
|
bandit -r semantica/ -ll | grep "Severity: High" -A 5 -B 1 || true
|
|
exit 1
|
|
else
|
|
echo "✅ No HIGH severity security issues found"
|
|
fi
|
|
|
|
- name: Run Semgrep (Static Analysis)
|
|
run: |
|
|
echo "Running Semgrep static analysis..."
|
|
semgrep --config=auto --json --output=semgrep-report.json semantica/ || true
|
|
|
|
# Run security-focused rules
|
|
echo "Checking for security patterns..."
|
|
SECURITY_ISSUES=$(semgrep --config=p/security --json semantica/ 2>/dev/null | jq '.results | length' 2>/dev/null || echo "0")
|
|
|
|
if [ "$SECURITY_ISSUES" -gt 0 ]; then
|
|
echo "⚠️ Security patterns found: $SECURITY_ISSUES"
|
|
echo "Review these findings for potential improvements"
|
|
semgrep --config=p/security semantica/ || true
|
|
else
|
|
echo "✅ No security patterns found"
|
|
fi
|
|
|
|
- name: Upload Security Reports
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
|
with:
|
|
name: security-reports
|
|
retention-days: 14
|
|
path: |
|
|
safety-report.json
|
|
bandit-report.json
|
|
semgrep-report.json
|
|
|
|
- name: Comment PR with Security Results
|
|
if: github.event_name == 'pull_request'
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
|
|
// Renders one tool's findings as a section. `items` is already
|
|
// the list of pre-formatted "- `thing` in `where`" strings; this
|
|
// just handles the found/not-found/report-missing framing and
|
|
// collapses long lists into a <details> block so the comment
|
|
// doesn't turn into a wall of text.
|
|
function renderSection(title, reportPath, parse) {
|
|
let data;
|
|
try {
|
|
data = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
} catch (e) {
|
|
return [
|
|
`### ${title}`,
|
|
`⚠️ No report found at \`${reportPath}\` — the scan may have failed before producing output. Check the job logs.`,
|
|
].join('\n');
|
|
}
|
|
|
|
const items = parse(data);
|
|
if (items.length === 0) {
|
|
return [`### ${title}`, `✅ No findings.`].join('\n');
|
|
}
|
|
|
|
const lines = [`### ${title}`, `Found **${items.length}**.`, ''];
|
|
const shown = items.slice(0, 15);
|
|
if (items.length > 15) {
|
|
lines.push('<details>', '<summary>Show all findings</summary>', '');
|
|
lines.push(...items);
|
|
lines.push('', '</details>');
|
|
} else {
|
|
lines.push(...shown);
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
const safetySection = renderSection(
|
|
'Safety — dependency vulnerabilities',
|
|
'safety-report.json',
|
|
(data) => (data.vulnerabilities || []).map(
|
|
(v) => `- \`${v.package_name}==${v.analyzed_version}\`: ${v.vulnerability_id}` +
|
|
(v.CVE ? ` (${v.CVE})` : '') + ` — ${v.advisory || 'no advisory text'}`
|
|
)
|
|
);
|
|
|
|
const banditSection = renderSection(
|
|
'Bandit — HIGH-severity code issues',
|
|
'bandit-report.json',
|
|
(data) => (data.results || [])
|
|
.filter((issue) => issue.issue_severity === 'HIGH')
|
|
.map((issue) => `- \`${issue.test_name}\` in \`${issue.filename}:${issue.line_number}\``)
|
|
);
|
|
|
|
const semgrepSection = renderSection(
|
|
'Semgrep — static analysis patterns',
|
|
'semgrep-report.json',
|
|
(data) => (data.results || []).map(
|
|
(issue) => `- \`${issue.check_id}\` in \`${issue.path}:${issue.start?.line ?? '?'}\``
|
|
)
|
|
);
|
|
|
|
const comment = [
|
|
'# 🔒 Security Scan Results',
|
|
'',
|
|
safetySection,
|
|
'',
|
|
banditSection,
|
|
'',
|
|
semgrepSection,
|
|
'',
|
|
'---',
|
|
'',
|
|
'*This security scan runs automatically on source-code PRs and bi-weekly (skipped for doc/markdown-only changes).*',
|
|
'',
|
|
'📊 **Security Policy**: CI fails on Safety vulnerabilities and Bandit HIGH-severity findings. Semgrep findings above are informational and do not block merge.',
|
|
].join('\n');
|
|
|
|
try {
|
|
await github.rest.issues.createComment({
|
|
issue_number: context.issue.number,
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
body: comment,
|
|
});
|
|
console.log('✅ Security comment posted successfully');
|
|
} catch (error) {
|
|
console.log('⚠️ Could not post security comment:', error.message);
|
|
console.log('📋 Security scan results saved to artifacts');
|
|
}
|