10
Pelaporan, CVSS, dan Exporter
Paket reports/ masih berupa placeholder, jadi seluruh logika report berada di core/reporter_agent.py (reports/__init__.py:1-61). Exporter di core/exporters/ sengaja dibuat tipis: hanya menerjemahkan schema, tidak memanggil LLM, tidak memvalidasi ulang temuan, dan tidak mengubah PentestMemory (core/exporters/init.py:1-12).
Alur dari PentestMemory ke tiap sink
graph TD
MEM["PentestMemory.findings"] --> EXEC["ReporterAgent.execute"]
EXEC --> ES["generate_executive_summary"]
EXEC --> TF["generate_technical_findings"]
EXEC --> RM["generate_remediation_plan"]
EXEC --> AT["generate_ai_trace"]
ES --> FMT{"format"}
TF --> FMT
RM --> FMT
AT --> FMT
FMT -->|"markdown"| MD["_assemble_markdown_report"]
FMT -->|"html"| HTML["_assemble_html_report"]
FMT -->|"json"| JS["_assemble_json_report"]
MD --> CLI["cli/commands/report.py"]
HTML --> CLI
JS --> CLI
CLI --> SARIF["sarif.export"]
CLI --> DD["defectdojo.export"]
CLI --> SLACK["slack.build_payload + post"]
style MEM fill:#336791,color:#fff
style FMT fill:#f59e0b,color:#000
style MD fill:#2d7d46,color:#fff
style HTML fill:#2d7d46,color:#fff
style JS fill:#2d7d46,color:#fff
style SARIF fill:#8b5cf6,color:#fff
style DD fill:#8b5cf6,color:#fff
style SLACK fill:#8b5cf6,color:#fff
%% fig: Alur PentestMemory menjadi report lalu ke tiap sink
%% desc: ReporterAgent merakit empat bagian menjadi format markdown, html, atau json, lalu CLI menulis berkas dan memanggil exporter sarif, defectdojo, dan slack.ReporterAgent.execute menerima satu argumen format dan mengembalikan dict berisi content, format, session_id, target, serta timestamp (core/reporter_agent.py:27-762). CLI menulis report["content"] ke berkas lalu memilih ekstensi dari nama format (cli/commands/report.py:85-963). Setelah report utama selesai, setiap nilai --export dijalankan pada blok yang sama (cli/commands/report.py:98-1204).
Empat bagian report
| Bagian | Fungsi | Sumber data |
|---|---|---|
| Executive summary | generate_executive_summary | get_findings_summary, get_findings_by_severity |
| Technical findings | generate_technical_findings | _format_findings_detailed |
| Remediation plan | generate_remediation_plan | _format_findings_detailed dan daftar f.target unik |
| AI decision trace | generate_ai_trace | memory.ai_decisions, thinking_chain, current_phase |
generate_technical_findings dan generate_remediation_plan sama-sama memakai hasil _format_findings_detailed, sementara remediation menambahkan daftar affected system dari setiap f.target (core/reporter_agent.py:119-1465).
generate_executive_summary tidak meminta LLM menebak rating risiko; nilainya diturunkan dari hitungan temuan (core/reporter_agent.py:78-1176):
| Kondisi | risk_rating |
|---|---|
summary["critical"] > 0 | CRITICAL |
summary["high"] > 3 | HIGH |
summary["high"] > 0 atau summary["medium"] > 5 | MEDIUM |
| selain itu | LOW |
_format_findings_detailed mengirim deskripsi dan evidence utuh sampai 4000 karakter per field, bukan potongan 200 karakter seperti versi sebelumnya (core/reporter_agent.py:337-3707).
⚠️
REPORTER_TOKEN_COST_SECTION_PROMPTdiimpor di modul reporter tetapi tidak pernah dipakai, sehingga appendix "AI Usage & Cost Summary" tidak pernah muncul di report mana pun (core/reporter_agent.py:11-188).
Tiga format perakitan
format | Fungsi perakit | Ekstensi berkas di CLI |
|---|---|---|
markdown | _assemble_markdown_report | md |
html | _assemble_html_report | html |
json | _assemble_json_report | json |
| lainnya | raise ValueError(f"Unknown format: {format}") | — |
Report Markdown menyusun urutan tetap: Target Information, Executive Summary, tabel Findings Summary, Technical Findings, Remediation Plan, AI Decision Trace, dan Tools Executed dari _format_tool_executions (core/reporter_agent.py:171-2239). Report JSON menyimpan metadata, executive_summary, findings_summary, findings hasil asdict, technical_findings, remediation_plan, ai_trace, dan tool_executions, lalu diserialisasi dengan json.dumps(report, indent=2, default=str) (core/reporter_agent.py:297-32410). Report HTML merender bagian naratif lewat _markdown_to_html, yang memakai pustaka markdown dengan extension extra, tables, fenced_code, dan sane_lists; bila pustaka tidak terpasang, teks di-escape ke dalam <pre> (core/reporter_agent.py:383-40411).
⚠️ Baris Total pada report Markdown dan HTML dihitung dengan
len(self.memory.findings), sedangkan baris per severity memakaiget_findings_summary()yang melewatifalse_positive(core/reporter_agent.py:171-2239, core/memory.py:227-24212). Total bisa lebih besar daripada jumlah baris di atasnya ketika ada temuan yang sudah ditandai false positive.
Verifikasi ulang skor vektor CVSS
core/cvss_calculator.py ada karena LLM reporter menulis vektor CVSS untuk setiap finding, dan tanpa validasi angka itu masuk ke report klien apa adanya (core/cvss_calculator.py:1-1813). Hasil validasi dibungkus CvssResult yang membawa vector, base_score, severity, valid, dan error (core/cvss_calculator.py:42-5014).
graph LR
V["Finding.cvss_vector"] --> P["parse_and_score"]
P -->|"prefix salah / metrik kurang"| ERR["CvssResult valid=False + error"]
P --> B["_base_score"]
B --> R["_roundup"]
R --> S["_severity_band"]
S --> C{"abs(base_score - cvss_score) <= 0.11"}
C -->|"True"| OK["tanpa cvss_note"]
C -->|"False"| WARN["CVSS vector does not match claimed score"]
style V fill:#336791,color:#fff
style ERR fill:#dc2626,color:#fff
style WARN fill:#dc2626,color:#fff
style OK fill:#2d7d46,color:#fff
style C fill:#f59e0b,color:#000
style S fill:#8b5cf6,color:#fff
%% fig: Verifikasi vektor CVSS sebelum report ditulis
%% desc: Vektor dari LLM di-parse, skor dasar dihitung ulang, lalu dibandingkan dengan skor klaim memakai toleransi 0.11.Hanya delapan base metric yang wajib ada — AV, AC, PR, UI, S, C, I, A — dan prefix vektor harus cocok dengan ^CVSS:3\.[01]/, sehingga vektor 3.0 tetap diterima (core/cvss_calculator.py:27-3915). parse_and_score tidak pernah melempar exception: kegagalan diubah menjadi CvssResult dengan valid=False dan error terisi (core/cvss_calculator.py:53-8016). Pesan yang mungkin muncul dari _parse_metrics adalah malformed metric token: {p}, missing required metric: {key}, dan invalid Scope value: {...} (core/cvss_calculator.py:105-127). _base_score memakai persamaan v3.1 dengan tabel weight PR yang berbeda saat Scope berubah, lalu _roundup membulatkan ke atas per 0.1 (core/cvss_calculator.py:130-154, core/cvss_calculator.py:167-181).
Rentang base_score | severity |
|---|---|
0.0 | NONE |
< 4.0 | LOW |
< 7.0 | MEDIUM |
< 9.0 | HIGH |
>= 9.0 | CRITICAL |
Bandingkan lewat validate_against_claimed(vector, claimed_score, tolerance=0.11): skor klaim None selalu dianggap cocok, dan vektor tidak valid selalu mengembalikan matches=False (core/cvss_calculator.py:83-9920). _format_findings_detailed memakai hasilnya untuk menyisipkan penanda [⚠️ CVSS vector does not match claimed score — recompute before publishing] ke blok finding yang dikirim ke LLM (core/reporter_agent.py:337-3707). Perilaku hitung ini diuji dengan vektor referensi dari FIRST.org, misalnya AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H yang harus menghasilkan 9.8 dan severity CRITICAL (tests/test_cvss_calculator.py:11-1921).
⚠️ Docstring modul menjanjikan skor yang tidak cocok akan ditimpa dan field
cvss_score_unverifiedditandai (core/cvss_calculator.py:1-1813), tetapiFindingtidak memiliki field tersebut danvalidate_against_claimedtidak mengubah objek finding (core/memory.py:29-3822). Satu-satunya efeknya adalah catatan teks di prompt.
Exporter SARIF, DefectDojo, dan Slack
--export | Fungsi yang dipanggil | Keluaran |
|---|---|---|
sarif | sarif.export(memory) | report_{session_id}.sarif |
defectdojo | defectdojo.export(memory) | report_{session_id}.defectdojo.json |
slack | slack.build_payload lalu slack.post | HTTP status dari webhook |
Penulisan ketiga berkas dan pengiriman webhook berada di satu blok for pada CLI (cli/commands/report.py:98-1204).
SARIF
export menghasilkan satu runs[0] berisi tool.driver.rules, invocations, results, dan properties sesi (session_id, target, phase); dokumennya berupa dict biasa yang diserialisasi pemanggil (core/exporters/sarif.py:49-9523). Temuan dengan false_positive dilewati dan setiap rule baru didaftarkan sekali per rule_id (core/exporters/sarif.py:60-6624). _rule_id_for memilih CWE lebih dulu, lalu CVE, lalu {tool}/{slug-judul} (core/exporters/sarif.py:101-11225).
Finding.severity | result.level |
|---|---|
critical, high | error |
medium | warning |
low, info | note |
Pemetaan level beserta _VERSION = "2.1.0" dan _SCHEMA_URI ada di satu tempat (core/exporters/sarif.py:35-4626). Setiap result membawa ruleId, level, message.text, locations[].physicalLocation.artifactLocation.uri, properties, dan fingerprints; security-severity ditulis sebagai string satu desimal f"{f.cvss_score:.1f}" agar dibaca GitHub code-scanning (core/exporters/sarif.py:151-18727).
⚠️ Docstring di modul SARIF menyebut fingerprint
guardian/execution, sedangkan kode menulis keyguardian/execution/v1(core/exporters/sarif.py:151-18727). Konsumen yang mengunci nama key dari docstring tidak akan menemukan fingerprint-nya.
DefectDojo
export mengembalikan {"findings": [...]} dengan temuan non-FP saja, dan _SEVERITY_MAP mengubah severity menjadi bentuk berhuruf besar seperti Critical atau Info (core/exporters/defectdojo.py:22-3528). Field opsional hanya diisi bila datanya ada: cve, cvssv3_score, cvssv3, cwe sebagai integer hasil int(f.cwe[4:]), dan url dari f.target (core/exporters/defectdojo.py:38-6129). Deskripsi menggabungkan description, bagian ## Evidence yang dipotong 2000 karakter, dan ## Execution ID (core/exporters/defectdojo.py:64-7230).
Slack dan Discord
build_payload menyusun satu field text berisi judul sesi, target, satu baris hitungan per severity dengan emoji, lalu maksimal top_n=5 temuan teratas hasil sorting _severity_rank (core/exporters/slack.py:30-6631). Emoji dipilih dari _SEVERITY_EMOJI, misalnya :rotating_light: untuk critical dan :information_source: untuk info (core/exporters/slack.py:21-2732). post mengirim JSON lewat urllib.request dengan timeout=10 dan mengembalikan resp.status (core/exporters/slack.py:74-8433).
Menjalankan guardian report
| Opsi | Default | Keterangan |
|---|---|---|
--session, -s | wajib | Session ID yang dilaporkan |
--format, -f | markdown | markdown, html, atau json |
--output, -o | ./reports/report_{session_id}.{ext} | Path berkas report |
--config, -c | config/guardian.yaml | Berkas konfigurasi |
--export, -e | kosong | Dapat diulang; sarif, defectdojo, slack |
--slack-webhook | env GUARDIAN_SLACK_WEBHOOK | Wajib saat --export slack |
Deklarasi opsi beserta _VALID_EXPORTS = {"sarif", "defectdojo", "slack"} ada di satu tempat (cli/commands/report.py:13-3834). Nilai --export divalidasi di awal agar proses gagal cepat: format asing mencetak Unknown export format '...' lalu typer.Exit(1) (cli/commands/report.py:59-6635). Bila --output tidak diberikan, ekstensi dipilih dari nama format, dan direktori induk dibuat dengan mkdir(parents=True, exist_ok=True) (cli/commands/report.py:85-963). Session dibaca dari ./reports/session_{session_id}.json hasil SessionPersister workflow; bila berkas tidak ada, perintah berhenti dengan pesan Session not found (cli/commands/report.py:68-7236).
⚠️
--export slacktanpa--slack-webhookmenghentikan perintah dengantyper.Exit(1), padahal berkas report utama sudah ditulis lebih dulu di blok try yang sama (cli/commands/report.py:98-1204).
Perilaku exporter diuji langsung pada PentestMemory buatan: fixture menambahkan satu temuan false_positive untuk memastikan temuan itu tidak ikut terekspor (tests/test_exporters.py:35-4737), bentuk dokumen dan pemetaan level diperiksa menjadi ["error", "warning"] (tests/test_exporters.py:50-6638), dan setiap result wajib punya guardian/execution/v1 serta rule CWE tanpa CVE wajib menunjuk cwe.mitre.org (tests/test_exporters.py:78-9139).