Situatie
În majoritatea companiilor, parolele, cheile API, token-urile și connection string-urile ajung în locuri unde nu ar trebui: hardcodate în scripturi PowerShell de pe share-uri, în fișiere appsettings.json urcate în Git, în istoricul commit-urilor „șterse” de mult sau în fișiere .env copiate din greșeală.
Scanerele de secrete (Gitleaks, TruffleHog) le găsesc, dar generează foarte mulți fals-pozitivi: parole de test, valori de exemplu din documentație, hash-uri, GUID-uri sau placeholder-e de tipul password = "CHANGE_ME". Echipa de securitate ajunge să ignore raportul, iar secretele reale se pierd în zgomot.
Soluția de mai jos combină:
- Gitleaks pentru detecție rapidă, bazată pe reguli și entropie;
- un LLM local (Ollama) care analizează contextul fiecărui rezultat și îl clasifică drept
REAL,TESTsauFALS_POZITIV; - un raport prioritizat în CSV/HTML, plus un hook pre-commit care oprește secretele înainte să ajungă în repo.
Toată analiza rulează on-premise. Secretele nu părăsesc infrastructura, iar LLM-ul primește doar valoarea mascată și contextul liniei.
Backup
Nu se modifică nimic în repo-uri sau pe share-uri: scanarea este doar de citire. Înainte de rescrierea istoricului Git cu git filter-repo, faceți o clonă --mirror a repo-ului.
Solutie
Pasi de urmat
Pasul 1 – Instalarea Gitleaks și Ollama
Pe stația sau serverul de scanare (Windows):
winget install --id Gitleaks.Gitleaks -e
winget install --id Ollama.Ollama -e
# Model mic și rapid, suficient pentru clasificare
ollama pull llama3.1:8b
Verificare:
gitleaks version
Invoke-RestMethod http://localhost:11434/api/tags | Select-Object -ExpandProperty models | Select-Object name
Pasul 2 – Configurare Gitleaks adaptată companiei
Creăm C:\SecretScan\.gitleaks.toml. Pornim de la regulile implicite și adăugăm reguli proprii (de exemplu connection string-uri SQL cu parolă) și excepții pentru căile care conțin doar date de test.
title = "Config Gitleaks companie"
[extend]
useDefault = true
[[rules]]
id = "sql-connection-string-password"
description = "Connection string SQL Server cu parolă în clar"
regex = '''(?i)(?:password|pwd)\s*=\s*([^;'"\s]{6,})'''
secretGroup = 1
keywords = ["password", "pwd"]
[[rules]]
id = "powershell-plaintext-securestring"
description = "ConvertTo-SecureString cu parolă în clar"
regex = '''(?i)ConvertTo-SecureString\s+(?:-String\s+)?["']([^"']{6,})["']\s+-AsPlainText'''
secretGroup = 1
keywords = ["convertto-securestring"]
[allowlist]
description = "Excepții globale"
paths = [
'''(?i)(^|/)tests?/''',
'''(?i)(^|/)samples?/''',
'''(?i)\.md$'''
]
regexes = [
'''(?i)(CHANGE_ME|<password>|your[_-]?password|xxxxxx)'''
]
Pasul 3 – Scanarea repo-urilor și a share-urilor
gitleaks gitscanează întregul istoric al unui repo, inclusiv commit-urile vechi.gitleaks dirscanează un director oarecare, de exemplu un share cu scripturi.
$cfg = "C:\SecretScan\.gitleaks.toml"
$out = "C:\SecretScan\raw"
New-Item -ItemType Directory -Path $out -Force | Out-Null
# Repo Git (cu istoric complet)
gitleaks git "D:\Repos\InfraScripts" --config $cfg `
--report-format json --report-path "$out\InfraScripts.json" --exit-code 0
# Share de rețea cu scripturi
gitleaks dir "\\fileserver\IT\Scripts" --config $cfg `
--report-format json --report-path "$out\Share-IT-Scripts.json" --exit-code 0
--exit-code 0 permite scriptului să continue chiar dacă găsește secrete. În pipeline-uri CI/CD lăsați valoarea implicită (1), ca build-ul să pice.
Pasul 4 – Triajul cu LLM local
Scriptul Invoke-SecretTriage.ps1 citește rapoartele Gitleaks, maschează secretul (păstrează doar primele și ultimele 2 caractere), extrage câteva linii de context din fișier și cere modelului o clasificare în format JSON.
param(
[string]$RawDir = "C:\SecretScan\raw",
[string]$OutCsv = "C:\SecretScan\raport-secrete.csv",
[string]$Model = "llama3.1:8b",
[string]$Ollama = "http://localhost:11434/api/generate"
)
function Mask-Secret([string]$s) {
if ($s.Length -le 6) { return ('*' * $s.Length) }
return $s.Substring(0,2) + ('*' * ($s.Length - 4)) + $s.Substring($s.Length - 2)
}
function Get-Context([string]$file, [int]$line, [string]$secret) {
if (-not (Test-Path $file)) { return "" }
$lines = Get-Content $file -ErrorAction SilentlyContinue
$from = [Math]::Max(0, $line - 4)
$to = [Math]::Min($lines.Count - 1, $line + 2)
# Secretul real nu ajunge în prompt, nici măcar prin context
($lines[$from..$to] -join "`n").Replace($secret, (Mask-Secret $secret))
}
$results = foreach ($report in Get-ChildItem $RawDir -Filter *.json) {
$findings = Get-Content $report.FullName -Raw | ConvertFrom-Json
foreach ($f in $findings) {
$masked = Mask-Secret $f.Secret
$context = Get-Context $f.File $f.StartLine $f.Secret
$prompt = @"
Ești analist de securitate. Clasifică rezultatul unui scaner de secrete.
Răspunde DOAR cu JSON valid de forma:
{"clasificare":"REAL|TEST|FALS_POZITIV","severitate":"CRITICA|RIDICATA|MEDIE|SCAZUTA","motiv":"max 25 cuvinte","actiune":"max 20 cuvinte"}
Regula: $($f.RuleID) - $($f.Description)
Fișier: $($f.File) (linia $($f.StartLine))
Secret (mascat): $masked, lungime $($f.Secret.Length), entropie $([math]::Round($f.Entropy,2))
Context:
$context
Criterii: valori ca 'test', 'example', 'dummy', localhost, fișiere de test => TEST.
Hash-uri, GUID-uri, ID-uri publice => FALS_POZITIV.
Chei cloud, parole de conturi de serviciu, token-uri în scripturi de producție => REAL.
"@
$body = @{ model = $Model; prompt = $prompt; stream = $false; format = "json";
options = @{ temperature = 0 } } | ConvertTo-Json -Depth 5
try {
$resp = Invoke-RestMethod -Uri $Ollama -Method Post -Body $body -ContentType "application/json; charset=utf-8"
$ai = $resp.response | ConvertFrom-Json
} catch {
$ai = [pscustomobject]@{ clasificare="NECLASIFICAT"; severitate="MEDIE"; motiv="Eroare LLM"; actiune="Verificare manuală" }
}
[pscustomobject]@{
Sursa = $report.BaseName
Fisier = $f.File
Linie = $f.StartLine
Commit = $f.Commit
Autor = $f.Author
Regula = $f.RuleID
SecretMascat = $masked
Clasificare = $ai.clasificare
Severitate = $ai.severitate
Motiv = $ai.motiv
Actiune = $ai.actiune
}
}
}
$ordine = @{ CRITICA = 0; RIDICATA = 1; MEDIE = 2; SCAZUTA = 3 }
$results |
Sort-Object @{ e = { if ($_.Clasificare -eq 'REAL') {0} else {1} } }, @{ e = { $ordine[$_.Severitate] } } |
Export-Csv $OutCsv -NoTypeInformation -Encoding UTF8
$real = ($results | Where-Object Clasificare -eq 'REAL').Count
Write-Host "Total rezultate: $($results.Count) | Secrete reale: $real | Raport: $OutCsv"
De ce temperatura 0 și format = "json": clasificarea trebuie să fie deterministă și ușor de parsat. Fără format = "json", modelele mici adaugă uneori text explicativ în jurul JSON-ului.
Pasul 5 – Raport HTML pentru echipa de securitate
$data = Import-Csv "C:\SecretScan\raport-secrete.csv" | Where-Object Clasificare -eq 'REAL'
$data | ConvertTo-Html -Title "Secrete expuse - $(Get-Date -f yyyy-MM-dd)" `
-PreContent "<h2>Secrete reale identificate: $($data.Count)</h2>" `
-Property Severitate, Sursa, Fisier, Linie, Autor, Regula, SecretMascat, Motiv, Actiune |
Out-File "C:\SecretScan\raport-secrete.html" -Encoding UTF8
Raportul HTML poate fi trimis pe email sau publicat pe un share intern cu acces restricționat.
Pasul 6 – Prevenție: hook pre-commit
Ca secretele să nu mai ajungă în repo, adăugați în fiecare repo fișierul .git/hooks/pre-commit (fără extensie):
#!/bin/sh
gitleaks git --pre-commit --staged --redact --verbose
if [ $? -ne 0 ]; then
echo "Commit blocat: au fost detectate secrete. Mutați-le în vault / variabile de mediu."
exit 1
fi
Pentru distribuire centralizată pe toate stațiile dezvoltatorilor se poate folosi framework-ul pre-commit sau un core.hooksPath setat prin GPO/Intune.
Pasul 7 – Rulare programată
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\SecretScan\Run-FullScan.ps1"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 6am
Register-ScheduledTask -TaskName "Scanare saptamanala secrete" -Action $action -Trigger $trigger `
-User "DOMENIU\svc-secretscan" -Password (Read-Host "Parola svc") -RunLevel Highest
Run-FullScan.ps1 conține pașii 3, 4 și 5 în ordine.
Ce faceți cu un secret REAL găsit
- Rotiți imediat secretul (parolă nouă, cheie API regenerată). Ștergerea din cod nu e suficientă, pentru că rămâne în istoricul Git.
- Verificați log-urile serviciului respectiv pentru utilizări suspecte ale vechiului secret.
- Mutați secretul într-un vault (Azure Key Vault, HashiCorp Vault sau modulul
Microsoft.PowerShell.SecretManagement). - Opțional, curățați istoricul cu
git filter-repo, apoi forțați re-clonarea repo-ului de către toată echipa.
Pe un set de 40 de repo-uri interne, Gitleaks singur poate genera sute de rezultate. După triajul AI, lista de verificat manual scade de obicei la câteva zeci de intrări marcate REAL, ordonate după severitate. Rezultatele marcate TEST și FALS_POZITIV merită verificate prin sondaj în primele săptămâni, apoi pot fi adăugate în allowlist.
Important: LLM-ul este un instrument de prioritizare, nu de decizie finală. Orice rezultat REAL cu severitate CRITICA trebuie confirmat de un om înainte de închidere.
Tip solutie
PermanentImpact colateral
Scanarea istoricului Git și a share-urilor generează încărcare de disc și CPU pe stația de scanare. Hook-ul pre-commit poate bloca commit-uri legitime până la ajustarea allowlist-ului.Plan de restaurare in caz de nefunctionare
Ștergeți task-ul programat (Unregister-ScheduledTask -TaskName "Scanare saptamanala secrete") și fișierul .git/hooks/pre-commit din repo-uri. Dezinstalați Gitleaks și Ollama cu winget uninstall dacă nu mai sunt necesare.
Leave A Comment?