Skip to content

Commit 11ce4e8

Browse files
committed
feat(result_parser): Better parsing of URI secrets
1 parent 09949e8 commit 11ce4e8

File tree

6 files changed

+116
-83
lines changed

6 files changed

+116
-83
lines changed

src/lib/api-types.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
export interface Occurrence {
23
type: string;
3-
// eslint-disable-next-line @typescript-eslint/naming-convention
44
line_start: number;
5-
// eslint-disable-next-line @typescript-eslint/naming-convention
65
index_start: number;
7-
// eslint-disable-next-line @typescript-eslint/naming-convention
86
line_end: number;
9-
// eslint-disable-next-line @typescript-eslint/naming-convention
107
index_end: number;
118
}
129

@@ -23,13 +20,9 @@ export interface Incident {
2320
type: string;
2421
occurrences: Occurrence[];
2522
validity: Validity;
26-
// eslint-disable-next-line @typescript-eslint/naming-convention
2723
ignore_sha: string;
28-
// eslint-disable-next-line @typescript-eslint/naming-convention
2924
known_secret: boolean;
30-
// eslint-disable-next-line @typescript-eslint/naming-convention
3125
incident_url: string;
32-
// eslint-disable-next-line @typescript-eslint/naming-convention
3326
total_occurrences: number;
3427
}
3528

@@ -41,6 +34,5 @@ export interface EntityWithIncidents {
4134
* JSON response from ggshield scan command
4235
*/
4336
export interface GGShieldScanResults {
44-
// eslint-disable-next-line @typescript-eslint/naming-convention
4537
entities_with_incidents: EntityWithIncidents[];
4638
}

src/lib/ggshield-api.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,13 @@ export async function scanFile(
173173
]);
174174

175175
if (proc.status === 128 || proc.status === 3) {
176-
let errorMessage = "";
177-
proc.stderr.split("\n").forEach((stderrLine) => {
178-
if (
179-
stderrLine.length > 0 &&
180-
!stderrLine.includes("Scanning Path...") // ggshield outputs this info message on stderr, ignore it
181-
) {
182-
errorMessage += stderrLine + "\n";
183-
}
184-
});
176+
const errorMessage = proc.stderr
177+
.split("\n")
178+
.filter(
179+
(stderrLine) =>
180+
stderrLine.length > 0 && !stderrLine.includes("Scanning Path...") // ggshield outputs this info message on stderr, ignore it
181+
)
182+
.join("\n");
185183
if (errorMessage.length > 0) {
186184
window.showErrorMessage(`ggshield: ${errorMessage}`);
187185
return undefined;
@@ -203,11 +201,11 @@ export async function scanFile(
203201
updateStatusBarItem(StatusBarStatus.noSecretFound);
204202
return;
205203
} else {
206-
const results = JSON.parse(proc.stdout);
207-
let incidentsDiagnostics: Diagnostic[] = parseGGShieldResults(results);
208204
updateStatusBarItem(StatusBarStatus.secretFound);
209-
diagnosticCollection.set(fileUri, incidentsDiagnostics);
210205
}
206+
const results = JSON.parse(proc.stdout);
207+
let incidentsDiagnostics: Diagnostic[] = parseGGShieldResults(results);
208+
diagnosticCollection.set(fileUri, incidentsDiagnostics);
211209
}
212210

213211
export async function loginGGShield(

src/lib/ggshield-results-parser.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/naming-convention */
12
import {
23
Diagnostic,
34
Range,
@@ -15,18 +16,29 @@ import {
1516

1617
const validityDisplayName: Record<Validity, string> = {
1718
unknown: "Unknown",
18-
// eslint-disable-next-line @typescript-eslint/naming-convention
1919
cannot_check: "Cannot Check",
20-
// eslint-disable-next-line @typescript-eslint/naming-convention
2120
no_checker: "No Checker",
22-
// eslint-disable-next-line @typescript-eslint/naming-convention
2321
failed_to_check: "Failed to Check",
24-
// eslint-disable-next-line @typescript-eslint/naming-convention
2522
not_checked: "Not Checked",
2623
invalid: "Invalid",
2724
valid: "Valid",
2825
};
2926

27+
/**
28+
* Given a list of occurrences, this function searches for the matches of type "connection_uri"
29+
* and returns it if found. If no "connection_uri" match is found, the original list is returned.
30+
* This ensures that only the full URI match is kept, avoiding multiple matches for its components (e.g. scheme, username, password, host).
31+
*
32+
* @param occurrences - An array of `Occurrence` objects to be filtered.
33+
* @returns An array containing the "connection_uri" occurrence, or the original list if no such match exists.
34+
*/
35+
function filterUriOccurrences(occurrences: Occurrence[]): Occurrence[] {
36+
const uriOccurrence = occurrences.find(
37+
({ type }) => type === "connection_uri"
38+
);
39+
return uriOccurrence ? [uriOccurrence] : occurrences;
40+
}
41+
3042
/**
3143
* Parse ggshield results and return diagnostics of found incidents
3244
*
@@ -45,27 +57,29 @@ export function parseGGShieldResults(
4557
results.entities_with_incidents.forEach(
4658
(entityWithIncidents: EntityWithIncidents) => {
4759
entityWithIncidents.incidents.forEach((incident: Incident) => {
48-
incident.occurrences.forEach((occurrence: Occurrence) => {
49-
let range = new Range(
50-
new Position(occurrence.line_start - 1, occurrence.index_start),
51-
new Position(occurrence.line_end - 1, occurrence.index_end)
52-
);
53-
let diagnostic = new Diagnostic(
54-
range,
55-
`ggshield: ${occurrence.type}
60+
filterUriOccurrences(incident.occurrences).forEach(
61+
(occurrence: Occurrence) => {
62+
let range = new Range(
63+
new Position(occurrence.line_start - 1, occurrence.index_start),
64+
new Position(occurrence.line_end - 1, occurrence.index_end)
65+
);
66+
let diagnostic = new Diagnostic(
67+
range,
68+
`ggshield: ${occurrence.type}
5669
5770
Secret detected: ${incident.type}
5871
Validity: ${validityDisplayName[incident.validity]}
5972
Known by GitGuardian dashboard: ${incident.known_secret ? "YES" : "NO"}
6073
Total occurrences: ${incident.total_occurrences}
6174
Incident URL: ${incident.incident_url || "N/A"}
6275
Secret SHA: ${incident.ignore_sha}`,
63-
DiagnosticSeverity.Warning
64-
);
76+
DiagnosticSeverity.Warning
77+
);
6578

66-
diagnostic.source = "gitguardian";
67-
diagnostics.push(diagnostic);
68-
});
79+
diagnostic.source = "gitguardian";
80+
diagnostics.push(diagnostic);
81+
}
82+
);
6983
});
7084
}
7185
);

src/test/constants.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,48 @@ export const scanResultsWithIncident = `{
3939
"total_occurrences":1,
4040
"secrets_engine_version":"2.96.0"
4141
}`;
42+
43+
export const scanResultsWithUriIncident = `{
44+
"id": "test.py",
45+
"type": "path_scan",
46+
"entities_with_incidents": [
47+
{
48+
"mode": "FILE",
49+
"filename": "test.py",
50+
"incidents": [
51+
{
52+
"policy": "Secrets detection",
53+
"occurrences": [
54+
{
55+
"match": "postgres:************************************4/thegift",
56+
"type": "connection_uri",
57+
"line_start": 1,
58+
"line_end": 1,
59+
"index_start": 16,
60+
"index_end": 70
61+
},
62+
{
63+
"match": "po****es",
64+
"type": "scheme",
65+
"line_start": 1,
66+
"line_end": 1,
67+
"index_start": 16,
68+
"index_end": 24
69+
}
70+
],
71+
"type": "PostgreSQL Credentials",
72+
"validity": "failed_to_check",
73+
"ignore_sha": "981de92451ea2b27f7a163d39c92b566de7cdcc52280fdd9cf7b331d6d53ad86",
74+
"total_occurrences": 1,
75+
"incident_url": "",
76+
"known_secret": false
77+
}
78+
],
79+
"total_incidents": 1,
80+
"total_occurrences": 1
81+
}
82+
],
83+
"total_incidents": 1,
84+
"total_occurrences": 1,
85+
"secrets_engine_version": "2.126.0"
86+
}`;

src/test/suite/lib/ggshield-api.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ suite("scanFile", () => {
7878
statusBar.StatusBarStatus.ignoredFile
7979
);
8080
});
81-
81+
8282
const errorCodes = [128, 3];
8383
errorCodes.forEach((code) => {
8484
test(`displays an error message if the scan command fails with error code ${code}`, async () => {
@@ -88,11 +88,15 @@ suite("scanFile", () => {
8888
stderr: "Error",
8989
});
9090

91-
await scanFile("test.py", Uri.file("test.py"), {} as GGShieldConfiguration);
91+
await scanFile(
92+
"test.py",
93+
Uri.file("test.py"),
94+
{} as GGShieldConfiguration
95+
);
9296

9397
// The error message is displayed
9498
assert.strictEqual(errorMessageMock.callCount, 1);
95-
assert.strictEqual(errorMessageMock.lastCall.args[0], "ggshield: Error\n");
99+
assert.strictEqual(errorMessageMock.lastCall.args[0], "ggshield: Error");
96100
});
97101
});
98102

src/test/suite/results-parser.test.ts

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,18 @@
11
import * as assert from "assert";
22

33
import { parseGGShieldResults } from "../../lib/ggshield-results-parser";
4-
import { window, DiagnosticSeverity } from "vscode";
5-
6-
const results = `{
7-
"id":"/Users/paulindenaurois/Development/gitguardian/ggshield-vscode-extension/sample_files/test.py",
8-
"type":"path_scan",
9-
"entities_with_incidents":[
10-
{
11-
"mode":"FILE",
12-
"filename":"/Users/paulindenaurois/Development/gitguardian/ggshield-vscode-extension/sample_files/test.py",
13-
"incidents":[
14-
{
15-
"policy":"Secrets detection",
16-
"occurrences":[
17-
{
18-
"match":"DDACC73DdB04********************************************057c78317C39",
19-
"type":"apikey",
20-
"line_start":4,
21-
"line_end":4,
22-
"index_start":11,
23-
"index_end":79,
24-
"pre_line_start":4,
25-
"pre_line_end":4
26-
}
27-
],
28-
"type":"Generic High Entropy Secret",
29-
"validity":"no_checker",
30-
"ignore_sha":"38353eb1a2aac5b24f39ed67912234d4b4a2e23976d504a88b28137ed2b9185e",
31-
"total_occurrences":1,
32-
"incident_url":"",
33-
"known_secret":false
34-
}
35-
],
36-
"total_incidents":1,
37-
"total_occurrences":1
38-
}
39-
],
40-
"total_incidents":1,
41-
"total_occurrences":1,
42-
"secrets_engine_version":"2.96.0"
43-
}`;
4+
import { DiagnosticSeverity } from "vscode";
5+
import {
6+
scanResultsNoIncident,
7+
scanResultsWithIncident,
8+
scanResultsWithUriIncident,
9+
} from "../constants";
4410

4511
suite("parseGGShieldResults", () => {
4612
test("Should parse ggshield scan output", () => {
47-
const diagnostics = parseGGShieldResults(JSON.parse(results));
13+
const diagnostics = parseGGShieldResults(
14+
JSON.parse(scanResultsWithIncident)
15+
);
4816
assert.strictEqual(diagnostics.length, 1);
4917
const diagnostic = diagnostics[0];
5018
assert.ok(diagnostic.message.includes("apikey"));
@@ -56,9 +24,21 @@ suite("parseGGShieldResults", () => {
5624
assert.strictEqual(diagnostic.severity, DiagnosticSeverity.Warning);
5725
});
5826

27+
test("Should return an empty array if there are no incidents", () => {
28+
const diagnostics = parseGGShieldResults(JSON.parse(scanResultsNoIncident));
29+
assert.strictEqual(diagnostics.length, 0);
30+
});
31+
5932
test("Should return an empty array on an invalid ggshield output", () => {
6033
const diagnostics = parseGGShieldResults(JSON.parse("{}"));
61-
6234
assert.strictEqual(diagnostics.length, 0);
6335
});
36+
37+
test("Should only return the 'connection_uri' match if the secret is an URI", () => {
38+
const diagnostics = parseGGShieldResults(
39+
JSON.parse(scanResultsWithUriIncident)
40+
);
41+
assert.strictEqual(diagnostics.length, 1);
42+
assert.ok(diagnostics[0].message.includes("connection_uri"));
43+
});
6444
});

0 commit comments

Comments
 (0)