feat: SARIF exporter for UI
Browse files- chitos_sarif.py +122 -0
chitos_sarif.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""치토스 진단 결과 → SARIF 2.1.0 변환기 (Shannon 흡수, 클린룸·OASIS 공개표준).
|
| 3 |
+
치토스 do_scan/do_deep 결과(dict)를 받아 CI/CD·DevSecOps 호환 SARIF 2.1.0 JSON으로 출력.
|
| 4 |
+
finding 형식: [sev('hi'|'mid'|'low'), [ko,en]title, [ko,en]detail]. good/matrix 등은 결과에 미포함.
|
| 5 |
+
AGPL 코드 미사용 — SARIF는 OASIS 공개표준이며 본 구현은 치토스 자체 코드다."""
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
_LEVEL = {"hi": "error", "mid": "warning", "low": "note"}
|
| 9 |
+
|
| 10 |
+
# 치토스 finding(영문 제목) 키워드 → OWASP Top 10 2021 · CWE 매핑 (공개표준)
|
| 11 |
+
_MAP = [
|
| 12 |
+
("security response headers", "A05:2021", "CWE-693"),
|
| 13 |
+
("hsts", "A02:2021", "CWE-319"),
|
| 14 |
+
("https", "A02:2021", "CWE-319"),
|
| 15 |
+
("cookie", "A05:2021", "CWE-1004"),
|
| 16 |
+
("cors", "A05:2021", "CWE-942"),
|
| 17 |
+
("csp", "A05:2021", "CWE-693"),
|
| 18 |
+
("sensitive", "A01:2021", "CWE-538"),
|
| 19 |
+
("backup", "A05:2021", "CWE-530"),
|
| 20 |
+
("server version", "A05:2021", "CWE-200"),
|
| 21 |
+
("x-powered-by", "A05:2021", "CWE-200"),
|
| 22 |
+
("tech stack", "A05:2021", "CWE-200"),
|
| 23 |
+
("tls certificate", "A02:2021", "CWE-295"),
|
| 24 |
+
("spf", "A07:2021", "CWE-290"),
|
| 25 |
+
("dkim", "A07:2021", "CWE-290"),
|
| 26 |
+
("dmarc", "A07:2021", "CWE-290"),
|
| 27 |
+
("dnssec", "A05:2021", "CWE-350"),
|
| 28 |
+
("caa", "A05:2021", "CWE-295"),
|
| 29 |
+
("mixed content", "A02:2021", "CWE-311"),
|
| 30 |
+
("external-script", "A08:2021", "CWE-353"),
|
| 31 |
+
("sri", "A08:2021", "CWE-353"),
|
| 32 |
+
("pii", "A01:2021", "CWE-359"),
|
| 33 |
+
("noindex", "A05:2021", "CWE-16"),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _owasp_cwe(en_title):
|
| 38 |
+
t = (en_title or "").lower()
|
| 39 |
+
for kw, owasp, cwe in _MAP:
|
| 40 |
+
if kw in t:
|
| 41 |
+
return owasp, cwe
|
| 42 |
+
return None, None
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _pick(v, li):
|
| 46 |
+
if isinstance(v, (list, tuple)) and len(v) == 2:
|
| 47 |
+
return v[li]
|
| 48 |
+
return str(v)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _slug(s):
|
| 52 |
+
return (re.sub(r"[^a-zA-Z0-9]+", "-", str(s).lower()).strip("-")[:60]) or "finding"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def to_sarif(d, lang="en"):
|
| 56 |
+
"""d = do_scan/do_deep 결과. lang: 'en'(기본, SARIF 권장) | 'ko'."""
|
| 57 |
+
li = 0 if lang == "ko" else 1
|
| 58 |
+
host = d.get("host", "")
|
| 59 |
+
base_uri = "https://%s/" % host if host else ""
|
| 60 |
+
# 오류/차단은 결과 없이 빈 run
|
| 61 |
+
findings = []
|
| 62 |
+
for k in ("sec", "seo", "geo"):
|
| 63 |
+
findings += list(d.get(k, []))
|
| 64 |
+
dp = d.get("deep", {})
|
| 65 |
+
findings += list(dp.get("issues", []))
|
| 66 |
+
|
| 67 |
+
# 비침투(do_scan/do_deep)는 관찰 기반 → proofState=observed.
|
| 68 |
+
# 능동 PoC로 증명된 finding은 호출 측에서 it에 'proven' 표식을 넣어 proofState=proven 처리(향후 능동엔진 연계).
|
| 69 |
+
proof = "proven" if d.get("proven") else "observed"
|
| 70 |
+
rules = {}
|
| 71 |
+
results = []
|
| 72 |
+
for it in findings:
|
| 73 |
+
try:
|
| 74 |
+
sev = it[0]
|
| 75 |
+
title = _pick(it[1], li)
|
| 76 |
+
detail = _pick(it[2], li)
|
| 77 |
+
en_title = _pick(it[1], 1)
|
| 78 |
+
rid = _slug(en_title) # ruleId는 영문 기준으로 언어 무관 고정
|
| 79 |
+
except Exception:
|
| 80 |
+
continue
|
| 81 |
+
lvl = _LEVEL.get(sev, "warning")
|
| 82 |
+
owasp, cwe = _owasp_cwe(en_title)
|
| 83 |
+
if rid not in rules:
|
| 84 |
+
rule = {
|
| 85 |
+
"id": rid,
|
| 86 |
+
"name": title,
|
| 87 |
+
"shortDescription": {"text": title},
|
| 88 |
+
"defaultConfiguration": {"level": lvl},
|
| 89 |
+
"properties": {"tags": [t for t in ["security", owasp, cwe] if t]},
|
| 90 |
+
}
|
| 91 |
+
if owasp or cwe:
|
| 92 |
+
rule["properties"]["owasp"] = owasp
|
| 93 |
+
rule["properties"]["cwe"] = cwe
|
| 94 |
+
rules[rid] = rule
|
| 95 |
+
results.append({
|
| 96 |
+
"ruleId": rid,
|
| 97 |
+
"level": lvl,
|
| 98 |
+
"message": {"text": "%s — %s" % (title, detail)},
|
| 99 |
+
"locations": [{"physicalLocation": {"artifactLocation": {"uri": base_uri or host or "target"}}}],
|
| 100 |
+
"properties": {"proofState": proof, "owasp": owasp, "cwe": cwe},
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
| 105 |
+
"version": "2.1.0",
|
| 106 |
+
"runs": [{
|
| 107 |
+
"tool": {"driver": {
|
| 108 |
+
"name": "CHITOS",
|
| 109 |
+
"informationUri": "https://final-bench-freeweb-security-scan.hf.space",
|
| 110 |
+
"version": "1.1",
|
| 111 |
+
"rules": list(rules.values()),
|
| 112 |
+
}},
|
| 113 |
+
"results": results,
|
| 114 |
+
"properties": {"host": host, "engine": "CHITOS", "nonIntrusive": True, "proofModel": proof},
|
| 115 |
+
}],
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == "__main__":
|
| 120 |
+
import json, sys
|
| 121 |
+
data = json.load(open(sys.argv[1], encoding="utf-8")) if len(sys.argv) > 1 else {"host": "example.com", "sec": [["mid", ["보안 헤더 누락", "Missing security headers"], ["꺼짐", "disabled"]]]}
|
| 122 |
+
print(json.dumps(to_sarif(data), ensure_ascii=False, indent=1))
|