Skip to content

Commit 0bcfb8c

Browse files
authored
Merge pull request #267 from six2dez/feat/expand-scanner-checks
feat: add 29 new vulnerability checks
2 parents 4aa6160 + d3e2ce0 commit 0bcfb8c

71 files changed

Lines changed: 6499 additions & 29 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { createMockRequest, createMockResponse, runCheck } from "engine";
2+
import { describe, expect, it } from "vitest";
3+
4+
import check from ".";
5+
6+
const OPENAI_TOKEN = "sk-proj-" + "A".repeat(42);
7+
const HF_TOKEN = "hf_" + "A".repeat(34);
8+
const GROQ_TOKEN = "gsk_" + "A".repeat(52);
9+
10+
describe("ai-key-disclosure", () => {
11+
it("does not run on non-200 response", async () => {
12+
const history = await runCheck(check, [
13+
{
14+
request: createMockRequest({
15+
id: "1",
16+
host: "example.com",
17+
method: "GET",
18+
path: "/",
19+
}),
20+
response: createMockResponse({
21+
id: "1",
22+
code: 500,
23+
body: OPENAI_TOKEN,
24+
}),
25+
},
26+
]);
27+
expect(history).toHaveLength(0);
28+
});
29+
30+
it("finds nothing on clean response", async () => {
31+
const history = await runCheck(check, [
32+
{
33+
request: createMockRequest({
34+
id: "1",
35+
host: "example.com",
36+
method: "GET",
37+
path: "/",
38+
}),
39+
response: createMockResponse({
40+
id: "1",
41+
code: 200,
42+
body: "normal content without secrets",
43+
}),
44+
},
45+
]);
46+
expect(history).toHaveLength(1);
47+
expect(history[0]?.steps[0]?.findings).toHaveLength(0);
48+
});
49+
50+
it("detects OpenAI key", async () => {
51+
const history = await runCheck(check, [
52+
{
53+
request: createMockRequest({
54+
id: "1",
55+
host: "example.com",
56+
method: "GET",
57+
path: "/",
58+
}),
59+
response: createMockResponse({
60+
id: "1",
61+
code: 200,
62+
body: `key=${OPENAI_TOKEN}`,
63+
}),
64+
},
65+
]);
66+
expect(history).toHaveLength(1);
67+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
68+
});
69+
70+
it("detects HuggingFace token", async () => {
71+
const history = await runCheck(check, [
72+
{
73+
request: createMockRequest({
74+
id: "1",
75+
host: "example.com",
76+
method: "GET",
77+
path: "/",
78+
}),
79+
response: createMockResponse({
80+
id: "1",
81+
code: 200,
82+
body: `token=${HF_TOKEN}`,
83+
}),
84+
},
85+
]);
86+
expect(history).toHaveLength(1);
87+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
88+
});
89+
90+
it("detects Groq key", async () => {
91+
const history = await runCheck(check, [
92+
{
93+
request: createMockRequest({
94+
id: "1",
95+
host: "example.com",
96+
method: "GET",
97+
path: "/",
98+
}),
99+
response: createMockResponse({
100+
id: "1",
101+
code: 200,
102+
body: `key=${GROQ_TOKEN}`,
103+
}),
104+
},
105+
]);
106+
expect(history).toHaveLength(1);
107+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
108+
});
109+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { defineRegexCheck, Severity } from "engine";
2+
3+
import { Tags } from "../../types";
4+
import { keyStrategy } from "../../utils/key";
5+
import { whenTextResponse } from "../../utils/when";
6+
7+
export default defineRegexCheck({
8+
id: "ai-key-disclosure",
9+
name: "AI API Key Disclosed",
10+
description:
11+
"Detects API keys for AI services including OpenAI, Anthropic, HuggingFace, and Groq in HTTP responses",
12+
tags: [Tags.SECRET],
13+
severity: Severity.HIGH,
14+
patterns: [
15+
/\bsk-proj-[A-Za-z0-9_-]{40,}\b/,
16+
/\bsk-ant-api[0-9]{2}-[A-Za-z0-9_-]{95}\b/,
17+
/\bhf_[A-Za-z0-9]{34}\b/,
18+
/\bgsk_[A-Za-z0-9]{52}\b/,
19+
],
20+
dedupeKey: keyStrategy().withHost().withPort().withPath().build(),
21+
when: whenTextResponse,
22+
toFinding: (matches) => ({
23+
name: "AI API Key Disclosed",
24+
description: `AI service API keys detected in the response:\n${matches.map((m) => "- `" + m + "`").join("\n")}`,
25+
}),
26+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { createMockRequest, createMockResponse, runCheck } from "engine";
2+
import { describe, expect, it } from "vitest";
3+
4+
import check from ".";
5+
6+
describe("aws-key-disclosure", () => {
7+
it("does not run on non-200 response", async () => {
8+
const history = await runCheck(check, [
9+
{
10+
request: createMockRequest({
11+
id: "1",
12+
host: "example.com",
13+
method: "GET",
14+
path: "/",
15+
}),
16+
response: createMockResponse({
17+
id: "1",
18+
code: 404,
19+
body: "AKIAIOSFODNN7EXAMPLE1",
20+
}),
21+
},
22+
]);
23+
expect(history).toHaveLength(0);
24+
});
25+
26+
it("finds nothing on clean response", async () => {
27+
const history = await runCheck(check, [
28+
{
29+
request: createMockRequest({
30+
id: "1",
31+
host: "example.com",
32+
method: "GET",
33+
path: "/",
34+
}),
35+
response: createMockResponse({
36+
id: "1",
37+
code: 200,
38+
body: "normal content without secrets",
39+
}),
40+
},
41+
]);
42+
expect(history).toHaveLength(1);
43+
expect(history[0]?.steps[0]?.findings).toHaveLength(0);
44+
});
45+
46+
it("detects AKIA access key", async () => {
47+
const history = await runCheck(check, [
48+
{
49+
request: createMockRequest({
50+
id: "1",
51+
host: "example.com",
52+
method: "GET",
53+
path: "/",
54+
}),
55+
response: createMockResponse({
56+
id: "1",
57+
code: 200,
58+
body: "aws_key=AKIAIOSFODNN7EXAMPLE",
59+
}),
60+
},
61+
]);
62+
expect(history).toHaveLength(1);
63+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
64+
});
65+
66+
it("detects ASIA temporary access key", async () => {
67+
const history = await runCheck(check, [
68+
{
69+
request: createMockRequest({
70+
id: "1",
71+
host: "example.com",
72+
method: "GET",
73+
path: "/",
74+
}),
75+
response: createMockResponse({
76+
id: "1",
77+
code: 200,
78+
body: "temp_key=ASIA1234567890ABCDEF",
79+
}),
80+
},
81+
]);
82+
expect(history).toHaveLength(1);
83+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
84+
});
85+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { defineRegexCheck, Severity } from "engine";
2+
3+
import { Tags } from "../../types";
4+
import { keyStrategy } from "../../utils/key";
5+
import { whenTextResponse } from "../../utils/when";
6+
7+
export default defineRegexCheck({
8+
id: "aws-key-disclosure",
9+
name: "AWS Key Disclosed",
10+
description:
11+
"Detects AWS access key IDs in HTTP responses that could allow unauthorized access to AWS services",
12+
tags: [Tags.SECRET, Tags.CLOUD],
13+
severity: Severity.CRITICAL,
14+
patterns: [
15+
/\b(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}\b/,
16+
],
17+
dedupeKey: keyStrategy().withHost().withPort().withPath().build(),
18+
when: whenTextResponse,
19+
toFinding: (matches) => ({
20+
name: "AWS Key Disclosed",
21+
description: `AWS access key IDs detected in the response:\n${matches.map((m) => "- `" + m + "`").join("\n")}`,
22+
}),
23+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { createMockRequest, createMockResponse, runCheck } from "engine";
2+
import { describe, expect, it } from "vitest";
3+
4+
import check from ".";
5+
6+
describe("azure-key-disclosure", () => {
7+
it("does not run on non-200 response", async () => {
8+
const history = await runCheck(check, [
9+
{
10+
request: createMockRequest({
11+
id: "1",
12+
host: "example.com",
13+
method: "GET",
14+
path: "/",
15+
}),
16+
response: createMockResponse({
17+
id: "1",
18+
code: 403,
19+
body: "AccountKey=dGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHN0cmluZyB0aGF0IG1lZXRz",
20+
}),
21+
},
22+
]);
23+
expect(history).toHaveLength(0);
24+
});
25+
26+
it("finds nothing on clean response", async () => {
27+
const history = await runCheck(check, [
28+
{
29+
request: createMockRequest({
30+
id: "1",
31+
host: "example.com",
32+
method: "GET",
33+
path: "/",
34+
}),
35+
response: createMockResponse({
36+
id: "1",
37+
code: 200,
38+
body: "normal content without secrets",
39+
}),
40+
},
41+
]);
42+
expect(history).toHaveLength(1);
43+
expect(history[0]?.steps[0]?.findings).toHaveLength(0);
44+
});
45+
46+
it("detects AccountKey pattern", async () => {
47+
const history = await runCheck(check, [
48+
{
49+
request: createMockRequest({
50+
id: "1",
51+
host: "example.com",
52+
method: "GET",
53+
path: "/",
54+
}),
55+
response: createMockResponse({
56+
id: "1",
57+
code: 200,
58+
body: "AccountKey=dGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHN0cmluZyB0aGF0IG1lZXRz",
59+
}),
60+
},
61+
]);
62+
expect(history).toHaveLength(1);
63+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
64+
});
65+
66+
it("detects full connection string", async () => {
67+
const history = await runCheck(check, [
68+
{
69+
request: createMockRequest({
70+
id: "1",
71+
host: "example.com",
72+
method: "GET",
73+
path: "/",
74+
}),
75+
response: createMockResponse({
76+
id: "1",
77+
code: 200,
78+
body: "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=dGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHN0cmluZyB0aGF0IG1lZXRz",
79+
}),
80+
},
81+
]);
82+
expect(history).toHaveLength(1);
83+
expect(history[0]?.steps[0]?.findings).toHaveLength(1);
84+
});
85+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { defineRegexCheck, Severity } from "engine";
2+
3+
import { Tags } from "../../types";
4+
import { keyStrategy } from "../../utils/key";
5+
import { whenTextResponse } from "../../utils/when";
6+
7+
export default defineRegexCheck({
8+
id: "azure-key-disclosure",
9+
name: "Azure Key Disclosed",
10+
description:
11+
"Detects Azure storage account keys and connection strings in HTTP responses",
12+
tags: [Tags.SECRET, Tags.CLOUD],
13+
severity: Severity.CRITICAL,
14+
patterns: [
15+
/(?:AccountKey|SharedAccessKey)\s*=\s*[A-Za-z0-9+/=]{44,88}/,
16+
/DefaultEndpointsProtocol=https?;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{44,88}/,
17+
],
18+
dedupeKey: keyStrategy().withHost().withPort().withPath().build(),
19+
when: whenTextResponse,
20+
toFinding: (matches) => ({
21+
name: "Azure Key Disclosed",
22+
description: `Azure storage credentials detected in the response:\n${matches.map((m) => "- `" + m + "`").join("\n")}`,
23+
}),
24+
});

0 commit comments

Comments
 (0)