2.9
Scan Diff dan Retest
Deep Eye memakai dua mekanisme identitas temuan yang berbeda: kunci identity (type, normalized_url, parameter) untuk diff dan --retest-new, serta fingerprint SHA-256 untuk dedupe. Ketiganya bertumpu pada normalisasi URL yang tidak sama, sehingga satu temuan bisa dianggap duplikat oleh dedupe tetapi tetap dihitung berbeda oleh diff. Untuk urutan fase scan yang lengkap, lihat Alur Scan; untuk bentuk laporan akhir, lihat Pelaporan dan Ekspor.
Dua kunci identitas
| Kunci | Fungsi pembentuk | Komponen | Normalisasi URL | Pemakai |
|---|---|---|---|---|
| identity | _identity / _identity_no_sev | type, url, parameter, dan severity hanya pada _identity | normalize_url — scheme dan host dihuruf-kecilkan, garis miring akhir path dibuang, query param diurutkan, fragment dibuang | diff_scans, --retest-new |
| fingerprint | fingerprint | type, path hasil normalize_path, parameter, root_cause_hint | normalize_path — hanya scheme://netloc/path, query dan fragment tidak ikut | dedupe_findings |
Pasangan _identity dan _identity_no_sev di core/scan_diff.py menjadi sumber kedua kunci diff; _identity menambahkan severity ke dalam tuple (core/scan_diff.py:31-471). normalize_url menurunkan scheme dan host ke huruf kecil, membuang garis miring di akhir path, mengurutkan hasil parse_qsl sebelum urlencode, lalu membuang fragment (core/scan_diff.py:10-282).
Sisi fingerprint memakai normalize_path yang hanya menyusun scheme://netloc/path tanpa query dan fragment (utils/finding_fingerprint.py:10-183). root_cause_hint merangkai type huruf kecil, payload maksimal 80 karakter, dan evidence maksimal 120 karakter, lalu merapatkan seluruh whitespace menjadi satu spasi (utils/finding_fingerprint.py:21-254). fingerprint menggabungkan type|normalize_path(url)|parameter|root_cause_hint menjadi satu string, menghitung SHA-256, dan mengambil 16 karakter hex pertama (utils/finding_fingerprint.py:28-385). identity_tuple menyediakan bentuk tiga elemen tanpa severity untuk kebutuhan lain (utils/finding_fingerprint.py:67-726).
graph LR
F["finding dict"] --> NU["normalize_url"]
F --> NP["normalize_path"]
F --> RCH["root_cause_hint"]
RCH --> FP["fingerprint sha256 16 hex"]
NP --> FP
NU --> ID["_identity_no_sev"]
NU --> IDS["_identity plus severity"]
ID --> RT["--retest-new filter"]
ID --> DS["diff_scans"]
IDS --> DS
FP --> DD["dedupe_findings"]
style FP fill:#336791,color:#fff
style DD fill:#2d7d46,color:#fff
style DS fill:#8b5cf6,color:#fff
style RT fill:#8b5cf6,color:#fff
style ID fill:#f59e0b,color:#000
style IDS fill:#f59e0b,color:#000
style NU fill:#f59e0b,color:#000
style NP fill:#f59e0b,color:#000
%% fig: Dua jalur identitas temuan
%% desc: normalize_url membentuk kunci identity untuk diff dan retest, sedangkan normalize_path menyusun bahan fingerprint untuk dedupe.Dedupe dengan fingerprint
dedupe_findings mengelompokkan temuan per fingerprint, menuliskan fingerprint ke setiap dict hasil, dan mempertahankan urutan kemunculan pertama; dengan default prefer_higher_severity=True, peringkat critical:4 sampai info:0 menentukan temuan mana yang disimpan (utils/finding_fingerprint.py:41-647). Karena fingerprint tidak memuat severity, dua temuan yang hanya berbeda severity tetap dianggap satu, dan yang tersisa adalah severity tertinggi (tests/test_features_1_19.py:29-338).
Pemanggilan terjadi di akhir scan: bila reporting.dedupe bernilai True, self.vulnerabilities diganti hasil dedupe_findings lalu severity_summary dihitung ulang (core/scanner_engine.py:851-8589). Berkas contoh menetapkan nilai itu true (config/config.example.yaml:39110).
Siklus status temuan
diff_scans lebih dulu membuang temuan yang tidak punya type atau url, lalu membangun indeks _identity dan indeks _identity_no_sev untuk kedua berkas (core/scan_diff.py:56-7111). Temuan dengan identity tanpa severity yang sama tetapi severity berbeda dicatat sebagai pasangan baseline/current di severity_changed, dan kuncinya dikumpulkan di sev_changed_no_sev_keys agar tidak ikut masuk ke new atau fixed (core/scan_diff.py:73-9312). Daftar unchanged adalah irisan kedua indeks (core/scan_diff.py:9513).
stateDiagram-v2
[*] --> ScanJSON: temuan ditulis ke vulnerabilities
ScanJSON --> New: identity tidak ada di baseline
ScanJSON --> Unchanged: identity dan severity sama
ScanJSON --> SeverityChanged: identity sama, severity berbeda
New --> Unchanged: terlihat lagi pada scan berikutnya
SeverityChanged --> Unchanged: severity kembali sama
Unchanged --> Fixed: identity hilang pada scan berikutnya
New --> Fixed: hilang sebelum sempat unchanged
Fixed --> [*]
style ScanJSON fill:#336791,color:#fff
style New fill:#dc2626,color:#fff
style Fixed fill:#2d7d46,color:#fff
style Unchanged fill:#8b5cf6,color:#fff
style SeverityChanged fill:#f59e0b,color:#000
%% fig: Perpindahan status temuan antar scan
%% desc: diff_scans menetapkan new, fixed, unchanged, atau severity_changed berdasarkan identity dan severity kedua berkas.Field summary | Arti |
|---|---|
new | identity ada di current dan tidak ada di baseline |
fixed | identity ada di baseline dan tidak ada di current |
unchanged | identity ada di kedua berkas |
severity_changed | identity tanpa severity sama, severity berbeda |
net_delta | new dikurangi fixed; nilai negatif berarti perbaikan |
Kelima nilai itu dihitung dari panjang daftar masing-masing, dengan net_delta sebagai len(new_list) - len(fixed_list) (core/scan_diff.py:97-10314). Hasil diff_scans mengembalikan kartu baseline dan current berisi target, scan_time yang jatuh ke start_time bila end_time kosong, serta vuln_count, di samping summary dan empat daftar temuan (core/scan_diff.py:105-12115).
⚠️ Indeks dibangun lewat dict comprehension
{_identity(v): v for v in ...}, jadi dua temuan dalam satu berkas scan dengan identity dan severity identik saling menimpa dan hanya kemunculan terakhir yang bertahan (core/scan_diff.py:64-6516). Hitungan diff bisa lebih kecil daripada jumlah baris di berkas scan.
Mode --diff
| Flag | Nilai | Keterangan |
|---|---|---|
--diff | BASELINE CURRENT | nargs=2, menjalankan diff tanpa scan |
--diff-output | path | default reports/diff_<timestamp>.<ext> |
--diff-format | html, json, csv | default html |
Ketiga flag itu dideklarasikan di parse_arguments (deep_eye.py:108-12817), dan main memeriksa args.diff lebih dulu sehingga scan dilewati sepenuhnya (deep_eye.py:248-25018). _run_diff_mode memuat kedua berkas via load_scan_json; FileNotFoundError atau json.JSONDecodeError menghasilkan pesan Failed to load scan JSON: {e} dan exit code 2. Bila --diff-output kosong, direktori reports dibuat dan nama berkas diturunkan dari timestamp serta format. Fungsi ini mencetak ringkasan New, Fixed, Severity Changed, Unchanged, dan Net Delta, lalu menutup dengan exit code 0 (deep_eye.py:193-23519). Alur ini juga diringkas di berkas rujukan (docs/SCAN_FLOW.md:71-8120).
Renderer keluaran
| Fungsi | Berkas | Ciri keluaran |
|---|---|---|
render_json | utils/exports/diff_renderer.py | json.dump dengan indent=2 dan ensure_ascii=False |
render_csv | utils/exports/diff_renderer.py | 9 kolom, diawali BOM \ufeff, satu baris per temuan berstatus new, fixed, unchanged, atau severity_changed |
render_html | utils/exports/diff_renderer.py | halaman mandiri dengan kartu meta, blok statistik, dan <details open> per daftar temuan |
render_csv menulis kolom status, type, severity, url, parameter, baseline_severity, current_severity, payload, evidence, dan setiap baris dirakit _row yang memotong evidence pada 500 karakter (utils/exports/diff_renderer.py:28-5121). render_csv sendiri menulis BOM lalu header sebelum baris data (utils/exports/diff_renderer.py:7-3822).
render_html menyusun kartu Baseline dan Current dari target, scan_time, dan vuln_count, empat blok statistik, lalu tabel terpisah untuk New Vulnerabilities dan Fixed Vulnerabilities serta tabel Severity Changed berisi panah baseline → current (utils/exports/diff_renderer.py:54-9923). Temuan unchanged tidak dibuatkan tabel, hanya disebut sebagai kalimat penutup berisi jumlahnya (utils/exports/diff_renderer.py:101-13824). Semua nilai melewati _esc yang mengganti &, <, >, dan ", sehingga type berisi <script> tampil sebagai teks (utils/exports/diff_renderer.py:141-14925).
--retest-new
Flag --retest-new BASELINE_JSON menyaring hasil scan yang baru selesai (deep_eye.py:130-13626). Setelah scanner.scan selesai, _identity_no_sev dipakai untuk membangun base_keys dari vulnerabilities baseline yang punya type dan url, lalu results['vulnerabilities'] disaring sehingga hanya temuan yang kuncinya tidak ada di baseline yang bertahan. Proses ini mencetak Retest-new: kept N/M new findings vs <path>; kegagalan apa pun ditangkap dan dilaporkan sebagai Retest-new skipped: {e} tanpa menghentikan pelaporan (deep_eye.py:349-36927).
⚠️
--retest-newmemakai_identity_no_sev, jadi temuan yang hanya berubah severity ikut dibuang bersama temuan lama. Filter ini cocok untuk pertanyaan CI "apa yang baru sejak run terakhir", tetapi bukan pengganti--diffyang tetap melaporkanseverity_changedsebagai kategori tersendiri.
Ringkasan perbedaan kedua kunci juga dicatat di dokumentasi arsitektur (docs/ARCHITECTURE.md:88-9428).