Add Level 0 + Level 1 + Level 2 security scanning #12

Merged
testclient-admin merged 5 commits from add-security-scanning into main 2026-08-23 16:25:11 +00:00
Showing only changes of commit 4777411360 - Show all commits
+44 -8
View File
@@ -154,6 +154,22 @@ jobs:
blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]] blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]]
blocked = len(blocking) > 0 blocked = len(blocking) > 0
# Один и тот же коммит может быть провёрен дважды (см. баг
# Gitea с переиспользованием SHA между разными PR — два
# параллельных прогона на идентичный коммит). Статус в
# Gitea хранит только ПОСЛЕДНЮЮ запись по (sha, context),
# так что более поздний, но менее строгий результат тихо
# затирает более ранний блокирующий. Раз уже была
# зафиксирована блокировка на этом коммите — не снимаем
# её молча, только новым коммитом.
prior_failed = False
try:
prior = gitea("GET", f"/repos/{REPO}/commits/{COMMIT_SHA}/statuses")
prior_failed = any(s.get("context") == "security/scan" and s.get("status") == "failure" for s in prior)
except Exception as e:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}"
os.makedirs(outdir, exist_ok=True) os.makedirs(outdir, exist_ok=True)
outpath = f"{outdir}/{int(time.time())}.json" outpath = f"{outdir}/{int(time.time())}.json"
@@ -177,19 +193,22 @@ jobs:
for f in top: for f in top:
body += f"**{f['severity'].upper()}** `{f['file']}:{f['line_start']}` — {f['title']} ({f['source']})\n\n" body += f"**{f['severity'].upper()}** `{f['file']}:{f['line_start']}` — {f['title']} ({f['source']})\n\n"
body += f"Полный список: `{outpath}`\n\n" body += f"Полный список: `{outpath}`\n\n"
if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне; новый прогон нашёл меньше, но старую блокировку это не снимает (нужен новый коммит).\n"
else:
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n" body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n"
gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body}) gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body})
gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", { gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", {
"state": "failure" if blocked else "success", "state": "failure" if (blocked or sticky) else "success",
"context": "security/scan", "context": "security/scan",
"description": f"{len(findings)} findings, {len(blocking)} blocking", "description": (f"{len(findings)} findings, {len(blocking)} blocking" + (" (sticky: prior run flagged this commit)" if sticky else "")),
"target_url": f"{SERVER}/{REPO}/src/branch/{HEAD_REF}/{outpath}", "target_url": f"{SERVER}/{REPO}/src/branch/{HEAD_REF}/{outpath}",
}) })
print(f"findings: {len(findings)}, blocking: {len(blocking)}") print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}")
if blocked: if blocked or sticky:
sys.exit(1) sys.exit(1)
PYEOF PYEOF
@@ -440,6 +459,20 @@ jobs:
blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]] blocking = [f for f in findings if f["severity"] in policy["block_on"] and f["confidence"] >= policy["min_confidence"]]
blocked = len(blocking) > 0 blocked = len(blocking) > 0
# См. аналогичный комментарий в шаге scan: LLM-ревьюер не
# детерминирован, а один коммит может быть провёрен дважды
# параллельно (баг Gitea с переиспользованием SHA между
# PR). Если более ранний прогон на ЭТОМ ЖЕ коммите уже
# нашёл блокирующую находку, более поздний "пустой" ответ
# модели не должен тихо снимать блокировку.
prior_failed = False
try:
prior = gitea("GET", f"/repos/{REPO}/commits/{COMMIT_SHA}/statuses")
prior_failed = any(s.get("context") == "security/review" and s.get("status") == "failure" for s in prior)
except Exception as e:
print(f"не удалось прочитать прежние статусы, продолжаю без sticky-fail: {e}")
sticky = prior_failed and not blocked
outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}" outdir = f"security-analysis/findings/pr-{PR_NUMBER or 'manual'}"
outpath = f"{outdir}/{int(time.time())}-review.json" outpath = f"{outdir}/{int(time.time())}-review.json"
@@ -467,19 +500,22 @@ jobs:
if f.get("exploit_scenario"): if f.get("exploit_scenario"):
body += f" сценарий: {f['exploit_scenario']}\n\n" body += f" сценарий: {f['exploit_scenario']}\n\n"
body += f"Полный список: `{outpath}`\n\n" body += f"Полный список: `{outpath}`\n\n"
if sticky:
body += "🔴 **Merge заблокирован** — этот же коммит уже был помечен как блокирующий в более раннем прогоне ревью; новый прогон ничего не нашёл, но старую блокировку это не снимает (нужен новый коммит).\n"
else:
body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n" body += "🔴 **Merge заблокирован** — есть находки выше порога `security-analysis/policy.yml`.\n" if blocked else "✅ Порог не превышен.\n"
gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body}) gitea("POST", f"/repos/{REPO}/issues/{PR_NUMBER}/comments", {"body": body})
gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", { gitea("POST", f"/repos/{REPO}/statuses/{COMMIT_SHA}", {
"state": "failure" if blocked else "success", "state": "failure" if (blocked or sticky) else "success",
"context": "security/review", "context": "security/review",
"description": f"{len(findings)} findings, {len(blocking)} blocking", "description": (f"{len(findings)} findings, {len(blocking)} blocking" + (" (sticky: prior run flagged this commit)" if sticky else "")),
"target_url": f"{SERVER}/{REPO}", "target_url": f"{SERVER}/{REPO}",
}) })
print(f"findings: {len(findings)}, blocking: {len(blocking)}") print(f"findings: {len(findings)}, blocking: {len(blocking)}, sticky: {sticky}")
if blocked: if blocked or sticky:
sys.exit(1) sys.exit(1)
PYEOF2 PYEOF2