Skip to content

Commit e22504c

Browse files
committed
feat(detector): HardcodedSecret Pattern-Match auf Token-Formate (Audit E-2 P2)
Bisher fand uHardcodedSecret nur Strings die einer Variable mit Secret- Keyword im Namen zugewiesen wurden (FPassword, ApiToken, ...). Das ueberging Tokens an "normalen" Variablen: FCfg := 'sk-prod-abc123...'; <-- nicht detected Url := 'https://api?key=AKIA1234...'; <-- (in einem Sub-String) nicht detected (nur als ganzer Match-RHS gepruef) Neue class function IsKnownSecretPattern(StrLit; out Kind: string). Erkennt token-spezifische Formate mit hoher Spezifitaet: * AWS Access Key: AKIA[0-9A-Z]{16} * GitHub Personal Access: ghp_[A-Za-z0-9]{36} * GitHub fine-grained: github_pat_[A-Z0-9]{22}_[A-Za-z0-9_]{59,} * OpenAI API Key: sk-(proj-|org-|svcacct-)?[A-Za-z0-9_-]{20,} * Google API Key: AIza[0-9A-Za-z_-]{35} * Slack Bot/User/Workspace: xox[bps]-[A-Za-z0-9-]{20,} * JWT (2/3 Segment): eyJ[A-Za-z0-9+/=_-]{10,}\.eyJ[A-Za-z0-9+/=_-]{10,} Aufruf: am Anfang der AnalyzeMethod-Schleife. Wenn der Wert wie ein bekanntes Token aussieht, wird ein Finding mit Confidence fcHigh und expliziter Kind-Bezeichnung erzeugt - Name-basierter Pfad wird geskippt (sonst Doppel-Findings). MIN_SECRET_LEN-Filter (16 chars) verhindert Trivial-Match auf kurze Strings. Mindest-Laenge ist im Pattern auch durch das {N,}-Limit abgesichert. Confidence fcHigh statt fcLow (wie der Name-basierter Pfad), weil die Patterns sehr spezifisch sind. False-Positive-Wahrscheinlichkeit gering: ein zufaelliger 36-Char-String der 'ghp_'-prefix matcht ist in der Praxis extrem unwahrscheinlich.
1 parent 9d760e9 commit e22504c

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

SCA.Engine/sources/Detectors/uHardcodedSecret.pas

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface
1919

2020
uses
2121
System.SysUtils, System.Generics.Collections,
22+
System.RegularExpressions,
2223
uAstNode, uSCAConsts, uMethodd12;
2324

2425
type
@@ -55,6 +56,24 @@ THardcodedSecretDetector = class
5556
// enthalten per Definition keine produktiven Secrets - Mock-Tokens,
5657
// Fixture-Passwoerter etc. werden hier nicht geflaggt.
5758
class function IsTestFilePath(const AFileName: string): Boolean; static;
59+
// 2026-06-18 (Audit_ErrorDetectors E-2 P2): Pattern-Match auf
60+
// String-Inhalt. Findet Secrets unabhaengig vom Variablen-Namen -
61+
// 'FCfg := ''sk-prod-...''' wird erkannt obwohl FCfg kein Secret-
62+
// Keyword im Namen hat.
63+
//
64+
// Patterns sind so spezifisch dass Confidence = fcHigh angemessen:
65+
// * AWS Access Key: AKIA[0-9A-Z]{16}
66+
// * GitHub PAT: ghp_[A-Za-z0-9]{36}
67+
// * GitHub fine-grained: github_pat_[A-Z]_[A-Za-z0-9]{82}
68+
// * OpenAI Key: sk-[A-Za-z0-9]{48} (auch sk-proj- / sk-org-)
69+
// * JWT (3-Segment): eyJ[A-Za-z0-9+/=_-]{10,}\.eyJ[A-Za-z0-9+/=_-]{10,}\.
70+
// * Slack Bot/User: xox[bps]-[A-Za-z0-9-]{20,}
71+
// * Google API: AIza[0-9A-Za-z_-]{35}
72+
//
73+
// True wenn StrLit eines der Patterns matched. AKind enthaelt einen
74+
// sprechenden Namen ('AWS Access Key' etc.) fuer die Finding-Message.
75+
class function IsKnownSecretPattern(const StrLit: string;
76+
out AKind: string): Boolean; static;
5877
end;
5978

6079
implementation
@@ -272,11 +291,28 @@ class procedure THardcodedSecretDetector.AnalyzeMethod(MethodNode: TAstNode;
272291
A : TAstNode;
273292
F : TLeakFinding;
274293
LitShort : string;
294+
PatKind : string;
275295
begin
276296
Assigns := MethodNode.FindAll(nkAssign);
277297
try
278298
for A in Assigns do
279299
begin
300+
// Pattern-Match-Pfad: Inhalt sieht aus wie AWS/GitHub/JWT/OpenAI...
301+
// Unabhaengig vom Variablen-Namen. Confidence fcHigh weil die
302+
// Patterns sehr spezifisch sind (Mindestlaenge + Prefix + Charset).
303+
if IsStringLiteral(A.TypeRef) and
304+
IsKnownSecretPattern(A.TypeRef, PatKind) then
305+
begin
306+
if Length(A.TypeRef) > MAX_VAL_LEN then
307+
LitShort := Copy(A.TypeRef, 1, MAX_VAL_LEN - 4) + '...'''
308+
else
309+
LitShort := A.TypeRef;
310+
Results.Add(TLeakFinding.New(FileName, MethodNode.Name, A.Line,
311+
PatKind + ' detected in literal: ' + A.Name + ' = ' + LitShort,
312+
fkHardcodedSecret, fcHigh));
313+
Continue; // Name-basierter Pfad wuerde Doppel-Finding produzieren
314+
end;
315+
280316
if not IsSecretName(A.Name) then Continue;
281317
if not IsStringLiteral(A.TypeRef) then Continue;
282318
// Leeres Literal '' ist Initialisierung, kein hartcodiertes Secret.
@@ -317,6 +353,63 @@ class procedure THardcodedSecretDetector.AnalyzeMethod(MethodNode: TAstNode;
317353
end;
318354
end;
319355

356+
class function THardcodedSecretDetector.IsKnownSecretPattern(
357+
const StrLit: string; out AKind: string): Boolean;
358+
// Lazy-Compile mit static-Local-Pattern - in einem Hot-Path waere ein
359+
// Cache angebracht; hier wird die Func nur in der AnalyzeMethod-Schleife
360+
// pro nkAssign gerufen, also vermutlich nicht hot genug fuer Premature-Opt.
361+
// Regex-Compile bei jedem Aufruf akzeptabel weil die Pattern simpel sind.
362+
//
363+
// StrLit ist der Roh-RHS aus N.TypeRef, inklusive umschliessender ' '.
364+
// Wir trimmen sie hier ein und matchen gegen den Body.
365+
const
366+
// Mindestens 16 chars um Trivial-Strings ('test', 'abc') auszufiltern.
367+
MIN_SECRET_LEN = 16;
368+
var
369+
Body : string;
370+
i, n : Integer;
371+
begin
372+
Result := False;
373+
AKind := '';
374+
if Length(StrLit) < MIN_SECRET_LEN + 2 then Exit; // +2 fuer ' '
375+
// Body = Inhalt zwischen erstem und letztem ' Token. Vereinfacht: wenn
376+
// beginnt mit ' und endet mit ' -> Substring.
377+
if (StrLit[1] <> '''') or (StrLit[Length(StrLit)] <> '''') then Exit;
378+
Body := Copy(StrLit, 2, Length(StrLit) - 2);
379+
if Length(Body) < MIN_SECRET_LEN then Exit;
380+
// '' (Doppel-Quote als Escape) wuerde im Match meist nicht vorkommen
381+
// weil Secret-Tokens reine [A-Za-z0-9+/=_.-]-Sets sind. Defensive
382+
// Replacement nicht noetig.
383+
384+
// AWS Access Key (always starts AKIA, 20 chars total)
385+
if TRegEx.IsMatch(Body, '^AKIA[0-9A-Z]{16}$') then
386+
begin AKind := 'AWS Access Key'; Exit(True); end;
387+
// GitHub Personal Access Token (classic, 40 chars)
388+
if TRegEx.IsMatch(Body, '^ghp_[A-Za-z0-9]{36}$') then
389+
begin AKind := 'GitHub Personal Access Token'; Exit(True); end;
390+
// GitHub fine-grained PAT
391+
if TRegEx.IsMatch(Body, '^github_pat_[A-Z0-9]{22}_[A-Za-z0-9_]{59,}$') then
392+
begin AKind := 'GitHub fine-grained Token'; Exit(True); end;
393+
// OpenAI API Key (sk-, sk-proj-, sk-org-)
394+
if TRegEx.IsMatch(Body,
395+
'^sk-(proj-|org-|svcacct-)?[A-Za-z0-9_-]{20,}$') then
396+
begin AKind := 'OpenAI API Key'; Exit(True); end;
397+
// Google API Key (AIza prefix, 39 chars total)
398+
if TRegEx.IsMatch(Body, '^AIza[0-9A-Za-z_-]{35}$') then
399+
begin AKind := 'Google API Key'; Exit(True); end;
400+
// Slack Bot/User/Workspace Token (xoxb-, xoxp-, xoxs-)
401+
if TRegEx.IsMatch(Body, '^xox[bps]-[A-Za-z0-9-]{20,}$') then
402+
begin AKind := 'Slack Token'; Exit(True); end;
403+
// JWT (3 Base64URL-segments getrennt durch Punkt; Header beginnt eyJ
404+
// weil { -> base64 = eyJ). Auch ohne Signatur-Segment akzeptiert
405+
// (manche use Cases haben 2-Segment-JWT).
406+
n := 0;
407+
for i := 1 to Length(Body) do if Body[i] = '.' then Inc(n);
408+
if (n in [1, 2]) and Body.StartsWith('eyJ') and
409+
TRegEx.IsMatch(Body, '^eyJ[A-Za-z0-9+/=_-]{10,}\.eyJ[A-Za-z0-9+/=_-]{10,}') then
410+
begin AKind := 'JWT Token'; Exit(True); end;
411+
end;
412+
320413
class function THardcodedSecretDetector.IsTestFilePath(
321414
const AFileName: string): Boolean;
322415
// Erkennt Test-Files anhand des Pfads. Convention-based:

0 commit comments

Comments
 (0)