mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
* ci: pin Python dependencies in requirements-ci.txt for reproducible CI Adds a committed lockfile pinning all transitive dependencies at exact versions (uv pip compile, Python 3.11, all extras — 1581 lines), the Python equivalent of explorer/package-lock.json + npm ci. - CI installs from requirements-ci.txt before building the wheel - CI verifies the lockfile is byte-identical to a fresh compile (fails on staleness after pyproject.toml changes) - CONTRIBUTING documents the regeneration command Closes #938 Signed-off-by: Yunare Maia <yunare@gmail.com> * ci: address Qodo review — security scans use pinned deps, exclude gpu extras - security-scan.yml installs from requirements-ci.txt instead of "./[llm-litellm]" so Safety scans the exact CI/release dependency tree - security.yml runs pip-audit -r requirements-ci.txt for the same parity - lockfile regenerated with --extra all (the cross-platform set) instead of --all-extras, which pulled faiss-gpu/cupy from the Linux-only gpu extra and co-installed faiss-cpu + faiss-gpu in CI - uv pinned to 0.12.1 (the version that generated the lockfile) in CI and CONTRIBUTING so regeneration is deterministic Signed-off-by: Yunare Maia <yunare@gmail.com> * ci: make lockfile staleness check immune to upstream releases The previous check re-resolved pyproject.toml without constraints, so any upstream package release (e.g. boto3 1.43.69 -> 1.43.70) failed CI even when nothing in the repo changed — exactly the time-dependent drift Qodo flagged. The check now re-resolves with requirements-ci.txt as a constraint and compares only version lines, so it detects intentional pyproject.toml changes but ignores upstream releases. CONTRIBUTING updated to match. Signed-off-by: Yunare Maia <yunare@gmail.com> * ci: fix security workflows — install pip-audit; order tooling after pinned deps Security workflow: the pip-audit install step was lost in the rebase conflict merge — pip-audit was invoked but never installed (exit 127). Security-scan workflow: installing safety first let the pinned requirements-ci.txt overwrite its transitive deps (rich), breaking the safety CLI at runtime (RuntimeError: Type not yet supported). Tooling is now installed AFTER the pinned set. Signed-off-by: Yunare Maia <yunare@gmail.com> * fix(ci): address review — hashes, build isolation, release builds, docs (4/4) ZohaibHassan16's review flagged 4 supply-chain gaps; all addressed: 1. **Release builds now use the lockfile**: release.yml installs requirements-ci.txt and runs `python -m build --no-isolation` so the sdist/wheel is built against the exact tested dependency set. 2. **Build isolation pinned**: [build-system].requires is now setuptools==84.0.0 + wheel==0.48.0 (exact pins, no ranges). 3. **Hashes**: requirements-ci.txt regenerated with --generate-hashes (5,708 sha256 hashes, verified against PyPI). Staleness check updated to strip the `\` line continuations hashes introduce. 4. **CONTRIBUTING.md documents the separate environment**: hashes, never-install-into-dev note, build-system pins, --no-isolation release builds. Validated: stale-check diff clean, hash spot-check matches PyPI. Signed-off-by: Yunare Maia <yunare@gmail.com> * fix(ci): apply --no-isolation to CI build + align benchmark to Python 3.11 Follow-up to ZohaibHassan16's second review round: 1. ci.yml was still running `python -m build` with build isolation (unpinned setuptools/wheel from PyPI) — now `python -m build --no-isolation` against the pinned deps, matching release.yml. 2. benchmark.yml was on Python 3.12 while the lockfile is compiled for 3.11 — aligned to 3.11 so every workflow runs the same environment. Signed-off-by: Yunare Maia <yunare@gmail.com> * fix(ci): install pinned wheel before --no-isolation build python -m build --no-isolation failed with 'Missing dependencies: wheel==0.48.0' because wheel is build-time only — uv's lockfile excludes it, so installing requirements-ci.txt alone left the build env without it. Both ci.yml and release.yml now install wheel==0.48.0 (the same pin [build-system] declares) before building. Validated locally: wheel builds clean with --no-isolation. Signed-off-by: Yunare Maia <yunare@gmail.com> --------- Signed-off-by: Yunare Maia <yunare@gmail.com> Co-authored-by: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com>
240 lines
9.9 KiB
YAML
240 lines
9.9 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: |
|
|
python -m pip install --upgrade pip
|
|
# 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
|
|
# 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 safety bandit semgrep jq
|
|
|
|
- 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');
|
|
}
|