Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bf61979
feat: add central gap-filling security workflows + trivy/osv reconcil…
Jul 8, 2026
5fabf12
fix(security): hash-pin pip installs in central python-security & sas…
Jul 8, 2026
6ddc74f
fix(security): mark clean OSV SARIF comprehensive
seonghobae Jul 11, 2026
844b396
fix(ci): scope PR workflow concurrency by PR number
seonghobae Jul 11, 2026
475c530
fix(noema): isolate workflow-run followup concurrency
seonghobae Jul 11, 2026
8b469c5
fix(security): preserve OSV scan analysis category
seonghobae Jul 11, 2026
271dfcd
fix(security): upload OSV clean SARIF to PR merge ref
seonghobae Jul 11, 2026
080cf35
fix(security): resolve PR merge SHA for OSV SARIF
seonghobae Jul 11, 2026
9969744
fix(security): upload OSV SARIF against PR head
seonghobae Jul 11, 2026
2857aec
fix(security): fetch PR merge ref before OSV SARIF upload
seonghobae Jul 11, 2026
ab64e9d
fix(security): upload OSV SARIF against PR head
seonghobae Jul 11, 2026
963d979
fix(security): keep Strix status publishing least privilege
seonghobae Jul 11, 2026
d142329
test(strix): align smoke with read-only status token
seonghobae Jul 11, 2026
3308105
fix(security): checkout PR merge ref before OSV upload
seonghobae Jul 11, 2026
ee343c0
fix(strix): restore trusted smoke status contract
seonghobae Jul 11, 2026
eef23ee
fix(security): pin OSV SARIF upload to PR head
seonghobae Jul 11, 2026
d79e230
fix: log concrete scheduler merge blockers
seonghobae Jul 11, 2026
2658edd
docs: record central ruleset approval gate
seonghobae Jul 11, 2026
7597427
fix(coverage): restore governance and docstring evidence
seonghobae Jul 11, 2026
3eab19e
test: align scorecard governance contract
seonghobae Jul 11, 2026
8c19679
ci: bound opencode model pool queue stalls
seonghobae Jul 11, 2026
cb89e0b
Merge remote-tracking branch 'origin/main' into codex-pr374-osv-sarif…
seonghobae Jul 11, 2026
35366b7
fix(strix): keep scan token status read-only
seonghobae Jul 11, 2026
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
3 changes: 1 addition & 2 deletions .github/workflows/close-empty-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ concurrency:
group: >-
close-empty-pr-${{
github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name || github.repository }}-${{
github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }}-${{
github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.run_id }}
github.event_name == 'pull_request_target' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: true

permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/noema-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ concurrency:
noema-review-${{
github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name ||
github.event_name == 'workflow_dispatch' && github.event.inputs.target_repository ||
github.repository }}-${{
github.repository }}-${{ github.event_name }}-${{
github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) ||
github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number ||
Expand Down
29 changes: 15 additions & 14 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ concurrency:
github.event_name == 'pull_request_target' && github.event.pull_request.base.repo.full_name ||
github.event_name == 'workflow_dispatch' && github.event.inputs.target_repository ||
github.repository }}-${{
github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) ||
github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number && github.event.inputs.pr_head_sha && format('pr-{0}-{1}', github.event.inputs.pr_number, github.event.inputs.pr_head_sha) ||
github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number && format('pr-{0}', github.event.inputs.pr_number) ||
github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number ||
github.run_id }}
Expand Down Expand Up @@ -2841,7 +2840,7 @@ jobs:
- name: Run OpenCode PR Review model pool
id: opencode_review_model_pool
if: needs.coverage-evidence.result == 'success'
timeout-minutes: 45
timeout-minutes: 12
continue-on-error: true
env:
STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }}
Expand All @@ -2857,26 +2856,28 @@ jobs:
SHARE: "false"
NPM_CONFIG_IGNORE_SCRIPTS: "true"
NO_COLOR: "1"
# High-sensitivity review candidates. Start with native OpenAI,
# then try compact GitHub Models fallbacks before full-size reasoning
# fallbacks so provider stalls still leave a visible
# MODEL_OUTPUT_UNAVAILABLE reason without exhausting the queue.
OPENCODE_MODEL_CANDIDATES: "openai/gpt-5-mini openai/gpt-5 github-models/deepseek/deepseek-v3-0324 github-models/openai/o4-mini github-models/openai/gpt-5-mini github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1 github-models/openai/o3 github-models/mistral-ai/mistral-medium-2505 github-models/meta/llama-4-maverick-17b-128e-instruct-fp8 github-models/meta/llama-4-scout-17b-16e-instruct"
# High-sensitivity review candidates only. Mini/nano/o*-mini models
# previously consumed the whole job on context-window timeouts before
# the pool reached stronger reviewers, so the default pool now starts
# with GPT-5/o3-class models, keeps provider-diverse full-size
# fallbacks, and bounds provider stalls so the org queue releases with
# a visible MODEL_OUTPUT_UNAVAILABLE reason.
OPENCODE_MODEL_CANDIDATES: "openai/gpt-5 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528"
# One attempt per model, then fall through to the next model. Retrying
# the SAME model 5x let a rate-limited/hung leader consume the whole
# step, so the pool never reached a healthy fallback model.
OPENCODE_MODEL_ATTEMPTS: "1"
# Ten minutes per model is enough for healthy providers to emit the
# Three minutes per model is enough for healthy providers to emit the
# required control block and short enough to avoid queue pileups when a
# provider stalls silently.
OPENCODE_RUN_TIMEOUT_SECONDS: "600"
OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "2400"
OPENCODE_RUN_TIMEOUT_SECONDS: "180"
OPENCODE_EXPORT_TIMEOUT_SECONDS: "60"
OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "540"
# Stop after one catalog pass; the retry budget should fail closed
# with visible diagnostics before the 350-min job timeout.
OPENCODE_POOL_MAX_CYCLES: "1"
OPENCODE_BACKOFF_INITIAL_SECONDS: "30"
OPENCODE_BACKOFF_MAX_SECONDS: "30"
OPENCODE_BACKOFF_INITIAL_SECONDS: "5"
OPENCODE_BACKOFF_MAX_SECONDS: "5"
OPENCODE_FIRST_ATTEMPT_AGENT: ci-review
OPENCODE_AGENT: ci-review-fallback
OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ on:
concurrency:
group: >-
central-pr-review-merge-scheduler-${{ github.repository }}-${{
github.event_name == 'pull_request_target' && format('pr-{0}-{1}', github.event.pull_request.number, github.event.pull_request.head.sha) ||
github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) ||
github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number) ||
github.event_name == 'workflow_call' && inputs.pr_number != '' && format('pr-{0}', inputs.pr_number) ||
github.event_name == 'workflow_call' && inputs.base_branch != '' && format('call-{0}', inputs.base_branch) ||
Expand Down
247 changes: 247 additions & 0 deletions .github/workflows/python-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Central Python security gate for every ContextualWisdomLab repo.
#
# Fills a governance gap left when duplicate LOCAL workflows were removed in
# favour of the central required workflows: the central Security Scan bundle
# covers supply-chain (osv / dependency-review / trivy) and posture (scorecard),
# but NOT Python source SAST (bandit) or the Python dependency audit
# (pip-audit) that some repos ran locally (naruon, bandscope,
# xtrmLLMBatchPython, contextual-orchestrator).
#
# bandit Python SAST -> SARIF uploaded under category "bandit"
# pip-audit dep audit -> HARD gate by job result (like osv/trivy)
#
# Both jobs are CONDITIONAL on the repo actually containing Python, so
# non-Python repos are a no-op. Gating is by the JOB result, ref-independent,
# exactly like the trivy-fs / osv-scan jobs in security-scan.yml. The SARIF is
# uploaded under a DISTINCT category ("bandit"); it is NOT added to the
# code_scanning ruleset rule, which stays CodeQL-only on purpose (requiring
# multiple tools in that rule is unsatisfiable across PR head/merge refs), so
# it does not affect auto-merge.
#
# High sensitivity: bandit fails on MEDIUM+ severity & MEDIUM+ confidence;
# pip-audit fails on any known vulnerability.
name: Python Security

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
branches: [main, master, develop]
push:
branches: [main, master, develop]
# Periodic full-repo coverage so non-PR drift is caught (the removed local
# workflows ran on push + schedule).
schedule:
- cron: "17 3 * * 1"
workflow_dispatch: {}

concurrency:
group: python-security-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
cancel-closed-pr-runs:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: echo "PR closed; this run only cancels older runs through workflow concurrency."

detect-python:
name: Detect Python
if: github.event.action != 'closed'
runs-on: ubuntu-latest
outputs:
has_python: ${{ steps.detect.outputs.has_python }}
has_manifest: ${{ steps.detect.outputs.has_manifest }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Detect Python sources and dependency manifests
id: detect
run: |
set -euo pipefail
has_python=false
if find . -type f -name '*.py' -not -path './.git/*' | head -1 | grep -q .; then
has_python=true
fi
has_manifest=false
if find . -type f \
\( -name 'requirements*.txt' -o -name 'pyproject.toml' \
-o -name 'pylock.*.toml' \) \
-not -path './.git/*' | head -1 | grep -q .; then
has_manifest=true
fi
echo "has_python=${has_python}" >> "$GITHUB_OUTPUT"
echo "has_manifest=${has_manifest}" >> "$GITHUB_OUTPUT"

bandit:
name: Bandit (Python SAST)
needs: detect-python
if: github.event.action != 'closed' && needs.detect-python.outputs.has_python == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
- name: Install bandit
# Hash-pinned lock (uv-generated) satisfies Scorecard Pinned-Dependencies.
run: python -m pip install --require-hashes -r requirements-bandit-ci-hashes.txt
- name: Run bandit (SARIF)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
id: bandit
run: |
set +e
bandit --recursive . \
--severity-level medium \
--confidence-level medium \
--exclude ./.git,./.github,./node_modules,./.venv,./venv,./tests,./test \
--format json \
--output bandit-results.json
echo "rc=$?" >> "$GITHUB_OUTPUT"
set -e
if [ ! -s bandit-results.json ]; then
echo "::error::Bandit did not produce bandit-results.json; inspect the Bandit command output above."
exit 2
fi
python - <<'PY'
import json
from pathlib import Path

data = json.loads(Path("bandit-results.json").read_text(encoding="utf-8"))
issues = data.get("results", [])
rules = {}
results = []
levels = {"HIGH": "error", "MEDIUM": "warning", "LOW": "note"}

for issue in issues:
rule_id = issue.get("test_id") or "bandit"
name = issue.get("test_name") or rule_id
text = issue.get("issue_text") or "Bandit finding"
severity = issue.get("issue_severity", "UNKNOWN")
confidence = issue.get("issue_confidence", "UNKNOWN")
filename = (issue.get("filename") or "").replace("\\", "/").lstrip("./")
line = int(issue.get("line_number") or 1)
message = f"{rule_id}: {text} (severity={severity}, confidence={confidence})"

print(f"::error file={filename},line={line},title={rule_id}::{message}")
rules.setdefault(
rule_id,
{
"id": rule_id,
"name": name,
"shortDescription": {"text": name},
"fullDescription": {"text": text},
"helpUri": issue.get("more_info", "https://bandit.readthedocs.io/"),
},
)
results.append(
{
"ruleId": rule_id,
"level": levels.get(severity, "warning"),
"message": {"text": message},
"locations": [
{
"physicalLocation": {
"artifactLocation": {"uri": filename},
"region": {"startLine": line},
}
}
],
}
)

print(f"Bandit findings at configured threshold: {len(issues)}")
sarif = {
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "Bandit",
"informationUri": "https://bandit.readthedocs.io/",
"rules": list(rules.values()),
}
},
"results": results,
}
],
}
Path("bandit-results.sarif").write_text(json.dumps(sarif, indent=2), encoding="utf-8")
PY
- name: Upload Bandit SARIF to code scanning
if: always() && hashFiles('bandit-results.sarif') != ''
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
sarif_file: bandit-results.sarif
category: bandit
- name: Enforce bandit gate (fail on MEDIUM+ findings)
if: steps.bandit.outputs.rc != '0'
run: |
echo "::error::Bandit found MEDIUM+ severity/confidence issues. See the 'bandit' code scanning category."
exit 1

pip-audit:
name: pip-audit (Python dependency audit)
needs: detect-python
if: github.event.action != 'closed' && needs.detect-python.outputs.has_manifest == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
- name: Install pip-audit
# Hash-pinned lock (uv-generated) satisfies Scorecard Pinned-Dependencies.
run: python -m pip install --require-hashes -r requirements-pip-audit-ci-hashes.txt
- name: Run pip-audit (hard gate on any known vulnerability)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
run: |
set -euo pipefail
status=0

# Audit every discovered requirements file.
while IFS= read -r req; do
echo "::group::pip-audit -r ${req}"
pip-audit --strict --desc=on -r "${req}" || status=1
echo "::endgroup::"
done < <(find . -type f -name 'requirements*.txt' -not -path './.git/*')

# Audit the project itself when a PEP 621 / lock manifest exists.
if find . -maxdepth 2 -type f \
\( -name 'pyproject.toml' -o -name 'pylock.*.toml' \) \
-not -path './.git/*' | head -1 | grep -q .; then
echo "::group::pip-audit . (project manifest)"
pip-audit --strict --desc=on . || status=1
echo "::endgroup::"
fi

if [ "${status}" != "0" ]; then
echo "::error::pip-audit reported known-vulnerable Python dependencies."
exit 1
fi
Loading
Loading