Mengapa False Positive SAST Merusak DevSecOps Pipeline
Tingginya angka false positive (FP) pada Static Application Security Testing (SAST) adalah penyebab utama tim engineering mengabaikan security alert. Ketika rule SAST terlalu agresif, tim developer terbebani oleh puluhan temuan tidak valid, memperlambat proses code review, dan akhirnya menurunkan adopsi security tooling secara keseluruhan.
Solusi teknis untuk masalah ini bukan menurunkan sensitivitas scanning di production secara manual, melainkan memberlakukan Rule-as-Code dengan automated test suite. Setiap modifikasi atau penambahan custom rule Semgrep harus melewati benchmark otomatis di CI (Continuous Integration) untuk memverifikasi precision dan recall sebelum di-merge ke repository sentral.
Struktur Fixture Test: True Positive vs True Negative
Semgrep menyediakan native harness untuk pengujian rule melalui anotasi komentar. Untuk setiap file rule (misalnya rules/auth/insecure-jwt-secret.yaml), letakkan file target pengujian dengan nama yang sama di direktori yang sama atau sub-direktori pengujian (misalnya rules/auth/insecure-jwt-secret.js).
Anotasi yang didukung adalah:
// ruleid: <rule-id>: Menandai baris kode yang wajib terdeteksi sebagai vulnerabilitas (True Positive).// ok: <rule-id>: Menandai baris kode aman yang sering memicu kesalahan pola AST, wajib tidak terdeteksi (True Negative).
Berikut adalah contoh implementasi custom rule Semgrep untuk mendeteksi hardcoded secret:
# File: rules/auth/insecure-jwt-secret.yaml
rules:
- id: insecure-jwt-secret
languages: [javascript, typescript]
severity: ERROR
message: "Hardcoded secret terdeteksi pada jwt.sign(). Gunakan environment variable."
patterns:
- pattern: jwt.sign($PAYLOAD, "...", ...)
- pattern-not: jwt.sign($PAYLOAD, "development-mock-secret", ...)
Berikut adalah file fixture pendamping untuk menguji validitas rule di atas:
// File: rules/auth/insecure-jwt-secret.js
const jwt = require("jsonwebtoken");
function issueToken(payload) {
// ruleid: insecure-jwt-secret
return jwt.sign(payload, "SUPER_SECRET_KEY_PROD_123", { expiresIn: "1h" });
}
function issueSafeToken(payload) {
// ok: insecure-jwt-secret
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" });
}
function issueMockToken(payload) {
// ok: insecure-jwt-secret
return jwt.sign(payload, "development-mock-secret", { expiresIn: "1h" });
}
Menjalankan Native semgrep --test
Semgrep memiliki command bawaan semgrep --test untuk memvalidasi test fixture terhadap rule definitions. Command ini mencocokkan baris temuan engine dengan posisi tag ruleid dan memastikan tidak ada temuan pada baris ok.
# Eksekusi unit test rule dan output hasil ke JSON
semgrep --test --json -o semgrep-test-results.json rules/
Jika terjadi ketidakcocokan (misalnya baris ok terdeteksi atau baris ruleid terlewat), CLI Semgrep secara native mengembalikan non-zero exit code. Namun, untuk pipeline CI yang membutuhkan audit threshold spesifik (misalnya: mentoleransi 0% False Positive tetapi mengizinkan threshold regresi tertentu), kita memerlukan skrip evaluasi terpisah.
Skrip Gating Python Stdlib untuk Metrik Presisi
Hindari dependensi eksternal berat di CI runner hanya untuk parsing metrik. Gunakan Python standard library untuk memverifikasi file JSON output dari Semgrep test runner dan mengeksekusi quality gate.
#!/usr/bin/env python3
# File: scripts/evaluate_rules.py
import json
import sys
def evaluate(report_path: str, max_fp_allowed: int = 0) -> None:
try:
with open(report_path, "r", encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
sys.stderr.write(f"Error: File {report_path} tidak ditemukan.
")
sys.exit(2)
# Parsing struktur internal semgrep test report
# Data memuat ringkasan eksekusi: checks dan results
stats = {
"passed": 0,
"false_positives": 0,
"false_negatives": 0
}
# Semgrep native test output format
results = data.get("results", [])
for entry in results:
if entry.get("passed") is True:
stats["passed"] += 1
else:
# Kegagalan deteksi: unexpected match (FP) atau missing match (FN)
for failure in entry.get("failures", []):
fail_type = failure.get("type")
if fail_type == "unexpected_match":
stats["false_positives"] += 1
print(f"[FP] Unexpected match di rule: {entry.get('rule_id')} baris {failure.get('line')}")
elif fail_type == "missing_match":
stats["false_negatives"] += 1
print(f"[FN] Missing match di rule: {entry.get('rule_id')} baris {failure.get('line')}")
total_issues = stats["false_positives"] + stats["false_negatives"]
print("--- Hasil Evaluasi Rule Semgrep ---")
print(f"Passed checks : {stats['passed']}")
print(f"False Positives : {stats['false_positives']}")
print(f"False Negatives : {stats['false_negatives']}")
# Quality Gate Policy
if stats["false_positives"] > max_fp_allowed:
sys.stderr.write(f"GATING FAILED: False Positive ({stats['false_positives']}) melebihi threshold ({max_fp_allowed}).
")
sys.exit(1)
if stats["false_negatives"] > 0:
sys.stderr.write("GATING FAILED: Terjadi regresi True Positive (False Negative > 0).
")
sys.exit(1)
print("GATING PASSED: Rule siap dideploy.")
sys.exit(0)
# ponytail: simple CLI parser, replace with argparse when adding export flags
if __name__ == "__main__":
target_report = sys.argv[1] if len(sys.argv) > 1 else "semgrep-test-results.json"
evaluate(target_report)
Workflow GitHub Actions dengan Caching
Konfigurasi workflow ini menjalankan benchmark hanya ketika ada perubahan pada direktori rules/ atau file skrip terkait. Tool cache dimanfaatkan untuk mencegah overhead instalasi Semgrep pada setiap trigger PR.
name: Semgrep Rule Benchmark
on:
pull_request:
paths:
- 'rules/**'
- 'scripts/evaluate_rules.py'
- '.github/workflows/semgrep-benchmark.yml'
jobs:
benchmark-rules:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Semgrep
run: |
pip install semgrep
- name: Run Semgrep Test Suite
run: |
# semgrep --test return exit code non-zero jika test gagal.
# Gunakan flag agar JSON tetap dihasilkan untuk dievaluasi gating script.
semgrep --test --json -o semgrep-test-results.json rules/ || true
- name: Evaluate Quality Gate
run: |
python scripts/evaluate_rules.py semgrep-test-results.json
Trade-offs dan Limitasi
1. Kompleksitas Taint Analysis
Rule berbasis mode: taint membutuhkan propagasi dari source ke sink. Test fixture sederhana terkadang lolos di test runner lokal jika definisi source dan sink berada dalam satu scope fungsi yang sama, namun menghasilkan FP tinggi pada framework arsitektur monolitik riil akibat sanitization step yang tidak terpetakan. Selalu sertakan test case dengan sanitizers di test fixture.
2. Sintaks Dinamis Bahasa Pemrograman
Bahasa pemrograman dinamis seperti JavaScript/Python memungkinkan aliasing impor modul. Pola import jwt from 'jsonwebtoken' berbeda penanganan AST-nya dengan dynamic import const jwt = await import('jsonwebtoken'). Pastikan suite fixture memuat ragam variasi penulisan impor yang lazim digunakan di internal codebase perusahaan.
3. Trade-off Precision vs Coverage
Menetapkan zero FP policy secara agresif dapat memicu under-detection (peningkatan False Negative). Aturan keamanan tingkat audit seringkali membutuhkan kelonggaran kontekstual. Pisahkan rule menjadi dua kategori:
- Blocking Rules (Zero FP threshold): Rule yang langsung memblokir PR developer di repo aplikasi. Wajib lolos 100% precision gate.
- Audit/Experimental Rules: Rule observasi yang dijalankan secara non-blocking untuk mengumpulkan data empiris sebelum dipromosikan ke blocking category.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!