Skip to content

Commit ffd99d0

Browse files
feat(scan): prevent scanning gitignored files
1 parent d7a4dca commit ffd99d0

File tree

5 files changed

+158
-4
lines changed

5 files changed

+158
-4
lines changed

src/gitguardian-interface/gitguardian-status-bar.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export enum StatusBarStatus {
1818
secretFound = "Secret found",
1919
noSecretFound = "No secret found",
2020
error = "Error",
21+
ignoredFile = "Ignored file",
2122
}
2223

2324
export function createStatusBarItem(context: ExtensionContext): void {
@@ -67,6 +68,11 @@ function getStatusBarConfig(status: StatusBarStatus): StatusBarConfig {
6768
color: "statusBarItem.errorBackground",
6869
command: "gitguardian.showOutput",
6970
};
71+
case StatusBarStatus.ignoredFile:
72+
return {
73+
text: "GitGuardian - Ignored file",
74+
color: "statusBarItem.warningBackground",
75+
};
7076
default:
7177
return { text: "", color: "statusBar.foreground" };
7278
}

src/lib/ggshield-api.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import axios from 'axios';
88
import { GGShieldConfiguration } from "./ggshield-configuration";
99
import { GGShieldScanResults } from "./api-types";
1010
import * as os from "os";
11-
import { apiToDashboard, dasboardToApi } from "../utils";
11+
import { apiToDashboard, dasboardToApi, isFileGitignored } from "../utils";
1212
import { runGGShieldCommand } from "./run-ggshield";
1313
import { StatusBarStatus, updateStatusBarItem } from "../gitguardian-interface/gitguardian-status-bar";
1414
import { parseGGShieldResults } from "./ggshield-results-parser";
1515

1616
/**
1717
* Extension diagnostic collection
1818
*/
19-
let diagnosticCollection: DiagnosticCollection;
19+
export let diagnosticCollection: DiagnosticCollection;
2020

2121
/**
2222
* Display API quota
@@ -153,6 +153,10 @@ export async function scanFile(
153153
fileUri: Uri,
154154
configuration: GGShieldConfiguration
155155
): Promise<void> {
156+
if (isFileGitignored(filePath)) {
157+
updateStatusBarItem(StatusBarStatus.ignoredFile);
158+
return;
159+
}
156160
const proc = runGGShieldCommand(configuration, [
157161
"secret",
158162
"scan",

src/test/constants.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const scanResultsNoIncident =
2+
'{"id": "test.py", "type": "path_scan", "total_incidents": 0, "total_occurrences": 0}';
3+
4+
export const scanResultsWithIncident = `{
5+
"id":"test.py",
6+
"type":"path_scan",
7+
"entities_with_incidents":[
8+
{
9+
"mode":"FILE",
10+
"filename":"test.py",
11+
"incidents":[
12+
{
13+
"policy":"Secrets detection",
14+
"occurrences":[
15+
{
16+
"match":"DDACC73DdB04********************************************057c78317C39",
17+
"type":"apikey",
18+
"line_start":4,
19+
"line_end":4,
20+
"index_start":11,
21+
"index_end":79,
22+
"pre_line_start":4,
23+
"pre_line_end":4
24+
}
25+
],
26+
"type":"Generic High Entropy Secret",
27+
"validity":"no_checker",
28+
"ignore_sha":"38353eb1a2aac5b24f39ed67912234d4b4a2e23976d504a88b28137ed2b9185e",
29+
"total_occurrences":1,
30+
"incident_url":"",
31+
"known_secret":false
32+
}
33+
],
34+
"total_incidents":1,
35+
"total_occurrences":1
36+
}
37+
],
38+
"total_incidents":1,
39+
"total_occurrences":1,
40+
"secrets_engine_version":"2.96.0"
41+
}`;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { GGShieldConfiguration } from "../../../lib/ggshield-configuration";
2+
import * as statusBar from "../../../gitguardian-interface/gitguardian-status-bar";
3+
import * as simple from "simple-mock";
4+
import { diagnosticCollection, scanFile } from "../../../lib/ggshield-api";
5+
import * as runGGShield from "../../../lib/run-ggshield";
6+
import path = require("path");
7+
import { Uri, window } from "vscode";
8+
import assert = require("assert");
9+
import {
10+
scanResultsNoIncident,
11+
scanResultsWithIncident,
12+
} from "../../constants";
13+
14+
suite("scanFile", () => {
15+
let updateStatusBarMock: simple.Stub<Function>;
16+
let runGGShieldCommandMock: simple.Stub<Function>;
17+
18+
setup(() => {
19+
updateStatusBarMock = simple.mock(statusBar, "updateStatusBarItem");
20+
runGGShieldCommandMock = simple.mock(runGGShield, "runGGShieldCommand");
21+
});
22+
23+
teardown(() => {
24+
simple.restore();
25+
});
26+
27+
test("successfully scans a file with no incidents", async () => {
28+
runGGShieldCommandMock.returnWith({
29+
status: 0,
30+
stdout: scanResultsNoIncident,
31+
stderr: "",
32+
});
33+
34+
await scanFile("test.py", Uri.file("test.py"), {} as GGShieldConfiguration);
35+
36+
// The status bar displays "No Secret Found"
37+
assert.strictEqual(updateStatusBarMock.callCount, 1);
38+
assert.strictEqual(
39+
updateStatusBarMock.lastCall.args[0],
40+
statusBar.StatusBarStatus.noSecretFound
41+
);
42+
});
43+
44+
test("successfully scans a file with incidents", async () => {
45+
runGGShieldCommandMock.returnWith({
46+
status: 0,
47+
stdout: scanResultsWithIncident,
48+
stderr: "",
49+
});
50+
51+
await scanFile("test.py", Uri.file("test.py"), {} as GGShieldConfiguration);
52+
53+
// The status bar displays "Secret Found"
54+
assert.strictEqual(updateStatusBarMock.callCount, 1);
55+
assert.strictEqual(
56+
updateStatusBarMock.lastCall.args[0],
57+
statusBar.StatusBarStatus.secretFound
58+
);
59+
60+
// The diagnostic collection contains the incident
61+
assert.strictEqual(
62+
diagnosticCollection.get(Uri.file("test.py"))?.length,
63+
1
64+
);
65+
});
66+
67+
test("skips the file if it is ignored", async () => {
68+
const filePath = "out/test.py";
69+
await scanFile(filePath, Uri.file(filePath), {} as GGShieldConfiguration);
70+
71+
// The status bar displays "Ignored File"
72+
assert.strictEqual(updateStatusBarMock.callCount, 1);
73+
assert.strictEqual(
74+
updateStatusBarMock.lastCall.args[0],
75+
statusBar.StatusBarStatus.ignoredFile
76+
);
77+
});
78+
79+
test("displays an error message if the scan command fails", async () => {
80+
const errorMessageMock = simple.mock(window, "showErrorMessage");
81+
runGGShieldCommandMock.returnWith({
82+
status: 1,
83+
stdout: "",
84+
stderr: "Error",
85+
});
86+
87+
await scanFile("test.py", Uri.file("test.py"), {} as GGShieldConfiguration);
88+
89+
// The error message is displayed
90+
assert.strictEqual(errorMessageMock.callCount, 1);
91+
assert.strictEqual(errorMessageMock.lastCall.args[0], "ggshield: Error\n");
92+
});
93+
});

src/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export async function isGitInstalled(): Promise<boolean> {
1818
});
1919
}
2020

21+
// Since git is required to use ggshield, we know that it is installed
22+
export function isFileGitignored(filePath: string): boolean {
23+
let proc = spawnSync("git", ["check-ignore", filePath]);
24+
return proc.status === 0;
25+
}
26+
2127
export function getCurrentFile(): string {
2228
const activeEditor = vscode.window.activeTextEditor;
2329
if (activeEditor) {
@@ -28,7 +34,9 @@ export function getCurrentFile(): string {
2834
}
2935

3036
export function dasboardToApi(dashboardUrl: string): string {
31-
const domainMatch = gitGuardianDomains.some((domain) => dashboardUrl.includes(domain));
37+
const domainMatch = gitGuardianDomains.some((domain) =>
38+
dashboardUrl.includes(domain)
39+
);
3240
if (domainMatch) {
3341
return dashboardUrl.replace(reDashboard, reApi);
3442
} else {
@@ -37,7 +45,9 @@ export function dasboardToApi(dashboardUrl: string): string {
3745
}
3846

3947
export function apiToDashboard(apiUrl: string): string {
40-
const domainMatch = gitGuardianDomains.some((domain) => apiUrl.includes(domain));
48+
const domainMatch = gitGuardianDomains.some((domain) =>
49+
apiUrl.includes(domain)
50+
);
4151
if (domainMatch) {
4252
return apiUrl.replace(reApi, reDashboard);
4353
} else {

0 commit comments

Comments
 (0)