security-mcp 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +966 -193
- package/defaults/agent-run-schema.json +98 -0
- package/dist/ci/pr-gate.js +18 -1
- package/dist/cli/install.js +69 -2
- package/dist/cli/onboarding.js +82 -11
- package/dist/cli/update.js +83 -15
- package/dist/gate/checks/ai-redteam.js +83 -59
- package/dist/gate/checks/api.js +93 -0
- package/dist/gate/checks/ci-pipeline.js +135 -0
- package/dist/gate/checks/crypto.js +91 -22
- package/dist/gate/checks/database.js +5 -1
- package/dist/gate/checks/dependencies.js +297 -2
- package/dist/gate/checks/dlp.js +6 -1
- package/dist/gate/checks/graphql.js +6 -1
- package/dist/gate/checks/k8s.js +229 -181
- package/dist/gate/checks/nuclei.js +133 -0
- package/dist/gate/checks/runtime.js +75 -8
- package/dist/gate/checks/scanners.js +8 -2
- package/dist/gate/diff.js +2 -0
- package/dist/gate/exceptions.js +6 -1
- package/dist/gate/policy.js +47 -4
- package/dist/gate/result.js +7 -1
- package/dist/mcp/audit-chain.js +253 -0
- package/dist/mcp/learning.js +228 -0
- package/dist/mcp/model-router.js +544 -0
- package/dist/mcp/orchestration.js +604 -0
- package/dist/mcp/server.js +160 -12
- package/dist/repo/search.js +5 -7
- package/dist/review/store.js +15 -0
- package/dist/types/agent-run.js +8 -0
- package/package.json +5 -5
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +225 -0
- package/skills/agentic-loop-exploiter/SKILL.md +69 -0
- package/skills/ai-llm-redteam/SKILL.md +118 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +198 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
- package/skills/android-penetration-tester/SKILL.md +83 -0
- package/skills/anti-replay-tester/SKILL.md +195 -0
- package/skills/appsec-code-auditor/SKILL.md +86 -0
- package/skills/artifact-integrity-analyst/SKILL.md +68 -0
- package/skills/attack-navigator/SKILL.md +64 -0
- package/skills/auth-session-hacker/SKILL.md +87 -0
- package/skills/aws-penetration-tester/SKILL.md +60 -0
- package/skills/azure-penetration-tester/SKILL.md +64 -0
- package/skills/binary-auth-validator/SKILL.md +184 -0
- package/skills/bot-detection-specialist/SKILL.md +221 -0
- package/skills/business-logic-attacker/SKILL.md +76 -0
- package/skills/capec-code-mapper/SKILL.md +163 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +200 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
- package/skills/ciso-orchestrator/SKILL.md +165 -0
- package/skills/cloud-infra-specialist/SKILL.md +85 -0
- package/skills/compliance-gap-analyst/SKILL.md +77 -0
- package/skills/compliance-grc/SKILL.md +148 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +169 -0
- package/skills/credential-stuffing-specialist/SKILL.md +192 -0
- package/skills/crypto-pki-specialist/SKILL.md +136 -0
- package/skills/csa-ccm-mapper/SKILL.md +178 -0
- package/skills/csf2-governance-mapper/SKILL.md +159 -0
- package/skills/deep-link-fuzzer/SKILL.md +195 -0
- package/skills/dependency-confusion-attacker/SKILL.md +78 -0
- package/skills/device-integrity-aggregator/SKILL.md +221 -0
- package/skills/dos-resilience-tester/SKILL.md +184 -0
- package/skills/dread-scorer/SKILL.md +157 -0
- package/skills/egress-policy-enforcer/SKILL.md +208 -0
- package/skills/evidence-collector/SKILL.md +86 -0
- package/skills/file-upload-attacker/SKILL.md +208 -0
- package/skills/gcp-penetration-tester/SKILL.md +63 -0
- package/skills/git-history-secret-scanner/SKILL.md +182 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +216 -0
- package/skills/incident-responder/SKILL.md +192 -0
- package/skills/injection-specialist/SKILL.md +62 -0
- package/skills/ios-security-auditor/SKILL.md +77 -0
- package/skills/json-ambiguity-tester/SKILL.md +175 -0
- package/skills/k8s-container-escaper/SKILL.md +74 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
- package/skills/kill-switch-engineer/SKILL.md +205 -0
- package/skills/linddun-privacy-analyst/SKILL.md +196 -0
- package/skills/logic-race-fuzzer/SKILL.md +67 -0
- package/skills/mobile-api-network-attacker/SKILL.md +81 -0
- package/skills/mobile-binary-hardener/SKILL.md +199 -0
- package/skills/mobile-security-specialist/SKILL.md +124 -0
- package/skills/mobile-webview-auditor/SKILL.md +200 -0
- package/skills/model-extraction-attacker/SKILL.md +68 -0
- package/skills/multipart-abuse-tester/SKILL.md +146 -0
- package/skills/oauth-pkce-specialist/SKILL.md +191 -0
- package/skills/parser-exhaustion-tester/SKILL.md +177 -0
- package/skills/pentest-infra/SKILL.md +69 -0
- package/skills/pentest-social/SKILL.md +72 -0
- package/skills/pentest-team/SKILL.md +126 -0
- package/skills/pentest-web-api/SKILL.md +71 -0
- package/skills/privacy-flow-analyst/SKILL.md +70 -0
- package/skills/prompt-injection-specialist/SKILL.md +76 -0
- package/skills/quantum-migration-planner/SKILL.md +184 -0
- package/skills/rag-poisoning-specialist/SKILL.md +71 -0
- package/skills/registry-mirror-enforcer/SKILL.md +142 -0
- package/skills/rotation-validation-agent/SKILL.md +188 -0
- package/skills/samm-assessor/SKILL.md +168 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +167 -0
- package/skills/senior-security-engineer/SKILL.md +42 -12
- package/skills/serialization-memory-attacker/SKILL.md +78 -0
- package/skills/session-timeout-tester/SKILL.md +197 -0
- package/skills/slsa-level3-enforcer/SKILL.md +185 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +181 -0
- package/skills/ssrf-detection-validator/SKILL.md +229 -0
- package/skills/step-up-auth-enforcer/SKILL.md +176 -0
- package/skills/stride-pasta-analyst/SKILL.md +72 -0
- package/skills/supply-chain-devsecops/SKILL.md +82 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +167 -0
- package/skills/threat-modeler/SKILL.md +116 -0
- package/skills/tls-certificate-auditor/SKILL.md +76 -0
- package/skills/token-reuse-detector/SKILL.md +203 -0
- package/skills/trike-risk-modeler/SKILL.md +139 -0
- package/skills/unicode-homograph-tester/SKILL.md +179 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +213 -0
- package/skills/webhook-security-tester/SKILL.md +184 -0
- package/skills/zero-trust-architect/SKILL.md +211 -0
package/dist/gate/checks/api.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
1
2
|
import { searchRepo } from "../../repo/search.js";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
2
5
|
export async function checkApi(_) {
|
|
3
6
|
const findings = [];
|
|
4
7
|
const zodHits = await searchRepo({ query: "zod|valibot|yup|joi", isRegex: true, maxMatches: 200 });
|
|
@@ -135,5 +138,95 @@ export async function checkApi(_) {
|
|
|
135
138
|
]
|
|
136
139
|
});
|
|
137
140
|
}
|
|
141
|
+
// 5. API schema drift (OpenAPI/Swagger spec vs code routes)
|
|
142
|
+
findings.push(...await checkApiSchemaDrift());
|
|
143
|
+
return findings;
|
|
144
|
+
}
|
|
145
|
+
function parseDeclaredPaths(specContent) {
|
|
146
|
+
const paths = new Set();
|
|
147
|
+
for (const match of specContent.matchAll(/^\s{0,4}(\/[a-zA-Z0-9/{}_-]+)\s*:/gm)) {
|
|
148
|
+
paths.add(match[1]);
|
|
149
|
+
}
|
|
150
|
+
return paths;
|
|
151
|
+
}
|
|
152
|
+
function findShadowRoutes(codeRouteHits, declaredPaths) {
|
|
153
|
+
const shadows = [];
|
|
154
|
+
for (const hit of codeRouteHits) {
|
|
155
|
+
const routeMatch = /['"](\/?[a-zA-Z0-9/{}_-]+)['"]/.exec(hit.preview);
|
|
156
|
+
if (!routeMatch)
|
|
157
|
+
continue;
|
|
158
|
+
const route = routeMatch[1].startsWith("/") ? routeMatch[1] : `/${routeMatch[1]}`;
|
|
159
|
+
const normalised = route.replaceAll(/:([a-zA-Z_]+)/g, "{$1}");
|
|
160
|
+
if (!declaredPaths.has(normalised) && !declaredPaths.has(route)) {
|
|
161
|
+
shadows.push(`${hit.file}:${hit.line} — ${route}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return shadows;
|
|
165
|
+
}
|
|
166
|
+
async function checkApiSchemaDrift() {
|
|
167
|
+
const findings = [];
|
|
168
|
+
try {
|
|
169
|
+
const specFiles = await fg([
|
|
170
|
+
"openapi.{yaml,yml,json}",
|
|
171
|
+
"swagger.{yaml,yml,json}",
|
|
172
|
+
"**/openapi.{yaml,yml,json}",
|
|
173
|
+
"**/swagger.{yaml,yml,json}",
|
|
174
|
+
"**/api-spec.{yaml,yml,json}",
|
|
175
|
+
"**/openapi/**/*.{yaml,yml,json}"
|
|
176
|
+
], { ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"], dot: true });
|
|
177
|
+
const codeRouteHits = await searchRepo({
|
|
178
|
+
query: String.raw `(?:router|app|fastify|server)\.(?:get|post|put|delete|patch)\s*\(\s*['"](/[^'"]+)['"]`,
|
|
179
|
+
isRegex: true,
|
|
180
|
+
maxMatches: 300
|
|
181
|
+
});
|
|
182
|
+
if (specFiles.length === 0) {
|
|
183
|
+
if (codeRouteHits.length > 0) {
|
|
184
|
+
findings.push({
|
|
185
|
+
id: "API_NO_OPENAPI_SPEC",
|
|
186
|
+
title: "API routes detected but no OpenAPI/Swagger specification found",
|
|
187
|
+
severity: "MEDIUM",
|
|
188
|
+
evidence: codeRouteHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
189
|
+
requiredActions: [
|
|
190
|
+
"Create an OpenAPI 3.x specification (openapi.yaml) that documents all API routes.",
|
|
191
|
+
"An API contract enables automated schema validation, client SDK generation, and drift detection.",
|
|
192
|
+
"Use tools like `zod-to-openapi` or `tsoa` to generate the spec from existing TypeScript code."
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
const specContent = await readFileSafe(specFiles[0]);
|
|
199
|
+
const declaredPaths = parseDeclaredPaths(specContent);
|
|
200
|
+
const shadowRoutes = findShadowRoutes(codeRouteHits, declaredPaths);
|
|
201
|
+
if (shadowRoutes.length > 0) {
|
|
202
|
+
findings.push({
|
|
203
|
+
id: "API_SHADOW_ENDPOINT",
|
|
204
|
+
title: `${shadowRoutes.length} API route(s) in code not declared in OpenAPI spec — shadow endpoints`,
|
|
205
|
+
severity: "HIGH",
|
|
206
|
+
evidence: [...new Set(shadowRoutes)].slice(0, 15),
|
|
207
|
+
requiredActions: [
|
|
208
|
+
"Add all undocumented routes to the OpenAPI specification.",
|
|
209
|
+
"Shadow endpoints bypass API gateway policies, rate limiting, and schema validation.",
|
|
210
|
+
"Automate spec generation (tsoa, zod-to-openapi) to prevent drift from recurring."
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (/type:\s+object/.test(specContent) && !/properties:/.test(specContent)) {
|
|
215
|
+
findings.push({
|
|
216
|
+
id: "API_PERMISSIVE_SCHEMA",
|
|
217
|
+
title: "OpenAPI spec contains `type: object` without `properties` — accepts any payload shape",
|
|
218
|
+
severity: "MEDIUM",
|
|
219
|
+
files: [specFiles[0]],
|
|
220
|
+
requiredActions: [
|
|
221
|
+
"Define explicit `properties` for all object schemas in the OpenAPI spec.",
|
|
222
|
+
"Permissive schemas allow attackers to inject unexpected fields (mass assignment, prototype pollution).",
|
|
223
|
+
"Set `additionalProperties: false` on request body schemas to enforce strict validation."
|
|
224
|
+
]
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
console.warn("[checkApiSchemaDrift] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
230
|
+
}
|
|
138
231
|
return findings;
|
|
139
232
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions CI/CD pipeline hardening checks.
|
|
3
|
+
* Covers supply chain attack vectors specific to GitHub Actions workflows.
|
|
4
|
+
*/
|
|
5
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
6
|
+
import fg from "fast-glob";
|
|
7
|
+
import { readFileSafe } from "../../repo/fs.js";
|
|
8
|
+
export async function runCiPipelineChecks(_opts) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
try {
|
|
11
|
+
const workflowFiles = await fg([".github/workflows/*.yml", ".github/workflows/*.yaml"], {
|
|
12
|
+
dot: true,
|
|
13
|
+
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
14
|
+
});
|
|
15
|
+
if (workflowFiles.length === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const unpinnedFiles = [];
|
|
19
|
+
const pwnTargetFiles = [];
|
|
20
|
+
const secretEchoFiles = [];
|
|
21
|
+
const noPermissionsFiles = [];
|
|
22
|
+
const selfHostedFiles = [];
|
|
23
|
+
for (const file of workflowFiles) {
|
|
24
|
+
let content;
|
|
25
|
+
try {
|
|
26
|
+
content = await readFileSafe(file);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// Check 1: Third-party actions not pinned to a full 40-char SHA
|
|
32
|
+
// Matches `uses: owner/repo@tag` but NOT `uses: owner/repo@<40hex chars>`
|
|
33
|
+
// Also skip `uses: ./.github/actions/` (local actions are fine)
|
|
34
|
+
const actionLines = content.split("\n").filter((line) => /uses:\s+[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+@/.test(line));
|
|
35
|
+
const unpinnedActions = actionLines.filter((line) => {
|
|
36
|
+
// Skip local actions
|
|
37
|
+
if (/uses:\s+\.\//.test(line))
|
|
38
|
+
return false;
|
|
39
|
+
// Flag anything not pinned to a 40-char hex SHA
|
|
40
|
+
return !/uses:\s+[a-zA-Z0-9_.\-/]+@[0-9a-f]{40}/.test(line);
|
|
41
|
+
});
|
|
42
|
+
if (unpinnedActions.length > 0) {
|
|
43
|
+
unpinnedFiles.push(file);
|
|
44
|
+
}
|
|
45
|
+
// Check 2: pull_request_target + dynamic ref usage (pwn-request vector)
|
|
46
|
+
// Attacker controls the ref/sha when a PR from a fork triggers pull_request_target
|
|
47
|
+
if (/pull_request_target/.test(content) &&
|
|
48
|
+
/\$\{\{\s*github\.event\.pull_request\.head\.(sha|ref)\s*\}\}/.test(content)) {
|
|
49
|
+
pwnTargetFiles.push(file);
|
|
50
|
+
}
|
|
51
|
+
// Check 3: Secrets printed to logs via echo
|
|
52
|
+
if (/echo\s+\$\{\{\s*secrets\./.test(content)) {
|
|
53
|
+
secretEchoFiles.push(file);
|
|
54
|
+
}
|
|
55
|
+
// Check 4: No top-level permissions block
|
|
56
|
+
// Without explicit permissions, the default is write access to all scopes
|
|
57
|
+
if (!/^permissions:/m.test(content)) {
|
|
58
|
+
noPermissionsFiles.push(file);
|
|
59
|
+
}
|
|
60
|
+
// Check 5: Self-hosted runners (broader attack surface — runner compromise = code execution)
|
|
61
|
+
if (/runs-on:\s+self-hosted/.test(content)) {
|
|
62
|
+
selfHostedFiles.push(file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (unpinnedFiles.length > 0) {
|
|
66
|
+
findings.push({
|
|
67
|
+
id: "CI_UNPINNED_ACTION",
|
|
68
|
+
title: "GitHub Actions using mutable tags instead of pinned SHA digests",
|
|
69
|
+
severity: "HIGH",
|
|
70
|
+
files: unpinnedFiles.slice(0, 10),
|
|
71
|
+
requiredActions: [
|
|
72
|
+
"Pin all third-party actions to a full 40-character commit SHA (e.g. `uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683`).",
|
|
73
|
+
"Mutable tags like @v3 can be silently redirected to malicious commits — SHA pinning prevents supply chain substitution.",
|
|
74
|
+
"Use a tool like `pin-github-action` or Dependabot to automate SHA pinning."
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (pwnTargetFiles.length > 0) {
|
|
79
|
+
findings.push({
|
|
80
|
+
id: "CI_PWNTARGET_SHA",
|
|
81
|
+
title: "pull_request_target workflow uses attacker-controlled ref/SHA — pwn-request vector",
|
|
82
|
+
severity: "CRITICAL",
|
|
83
|
+
files: pwnTargetFiles.slice(0, 10),
|
|
84
|
+
requiredActions: [
|
|
85
|
+
"Never use `${{ github.event.pull_request.head.sha }}` or `head.ref` inside a `pull_request_target` workflow that checks out or runs code from the PR.",
|
|
86
|
+
"`pull_request_target` runs with write permissions and secrets access; the PR head is attacker-controlled.",
|
|
87
|
+
"Use `pull_request` (not `pull_request_target`) for code that executes untrusted contributions, or add explicit guard conditions."
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (secretEchoFiles.length > 0) {
|
|
92
|
+
findings.push({
|
|
93
|
+
id: "CI_SECRET_ECHO",
|
|
94
|
+
title: "GitHub Actions workflow echoes secrets to logs",
|
|
95
|
+
severity: "CRITICAL",
|
|
96
|
+
files: secretEchoFiles.slice(0, 10),
|
|
97
|
+
requiredActions: [
|
|
98
|
+
"Remove any `echo ${{ secrets.* }}` statements — secrets printed to logs are visible to anyone with read access to the repository.",
|
|
99
|
+
"GitHub masks known secret values in logs, but this is not reliable for all encodings.",
|
|
100
|
+
"Pass secrets via environment variables (`env: MY_SECRET: ${{ secrets.MY_SECRET }}`) and read them in code, never echo them."
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (noPermissionsFiles.length > 0) {
|
|
105
|
+
findings.push({
|
|
106
|
+
id: "CI_NO_PERMISSIONS",
|
|
107
|
+
title: "GitHub Actions workflows without explicit permissions block",
|
|
108
|
+
severity: "MEDIUM",
|
|
109
|
+
files: noPermissionsFiles.slice(0, 10),
|
|
110
|
+
requiredActions: [
|
|
111
|
+
"Add an explicit `permissions:` block at the top of each workflow (or at the job level) to grant only the minimum required scopes.",
|
|
112
|
+
"Without explicit permissions, the default is determined by org/repo settings — often write-all.",
|
|
113
|
+
"Example minimal read-only: `permissions: { contents: read }`."
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (selfHostedFiles.length > 0) {
|
|
118
|
+
findings.push({
|
|
119
|
+
id: "CI_SELF_HOSTED_RUNNER",
|
|
120
|
+
title: "GitHub Actions using self-hosted runners",
|
|
121
|
+
severity: "MEDIUM",
|
|
122
|
+
files: selfHostedFiles.slice(0, 10),
|
|
123
|
+
requiredActions: [
|
|
124
|
+
"Self-hosted runners executing untrusted fork PRs can be compromised — restrict `pull_request` triggers on self-hosted runner workflows.",
|
|
125
|
+
"Ensure self-hosted runners are ephemeral (destroyed after each job) and isolated from production networks.",
|
|
126
|
+
"Use GitHub-hosted runners for public repositories or untrusted code paths."
|
|
127
|
+
]
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.warn("[runCiPipelineChecks] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
133
|
+
}
|
|
134
|
+
return findings;
|
|
135
|
+
}
|
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weak cryptography detection.
|
|
3
|
+
* Mapped to NIST SP 800-131A Rev 2.
|
|
4
|
+
*/
|
|
5
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
1
6
|
import { searchRepo } from "../../repo/search.js";
|
|
7
|
+
function checkPbkdf2Iterations(hits) {
|
|
8
|
+
for (const hit of hits) {
|
|
9
|
+
const iterMatch = /pbkdf2(?:Sync)?\s*\([^)]*?,\s*[^,]+,\s*(\d+)/.exec(hit.preview);
|
|
10
|
+
if (!iterMatch)
|
|
11
|
+
continue;
|
|
12
|
+
const iters = Number.parseInt(iterMatch[1], 10);
|
|
13
|
+
if (iters < 600000) {
|
|
14
|
+
return {
|
|
15
|
+
id: "CRYPTO_LOW_PBKDF2_ITERATIONS",
|
|
16
|
+
title: `PBKDF2 iteration count too low (${iters} < 600,000)`,
|
|
17
|
+
severity: "HIGH",
|
|
18
|
+
evidence: [`${hit.file}:${hit.line}:${hit.preview}`],
|
|
19
|
+
files: [hit.file],
|
|
20
|
+
requiredActions: [
|
|
21
|
+
"Use ≥ 600,000 iterations for PBKDF2-SHA256 (OWASP 2023 recommendation).",
|
|
22
|
+
"Prefer bcrypt (cost ≥ 12) or Argon2id instead."
|
|
23
|
+
]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
2
29
|
export async function checkCrypto(_opts) {
|
|
3
30
|
const findings = [];
|
|
4
31
|
try {
|
|
@@ -86,27 +113,9 @@ export async function checkCrypto(_opts) {
|
|
|
86
113
|
isRegex: true,
|
|
87
114
|
maxMatches: 200
|
|
88
115
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (iterMatch) {
|
|
93
|
-
const iters = parseInt(iterMatch[1], 10);
|
|
94
|
-
if (iters < 600000) {
|
|
95
|
-
findings.push({
|
|
96
|
-
id: "CRYPTO_LOW_PBKDF2_ITERATIONS",
|
|
97
|
-
title: `PBKDF2 iteration count too low (${iters} < 600,000)`,
|
|
98
|
-
severity: "HIGH",
|
|
99
|
-
evidence: [`${hit.file}:${hit.line}:${hit.preview}`],
|
|
100
|
-
files: [hit.file],
|
|
101
|
-
requiredActions: [
|
|
102
|
-
"Use ≥ 600,000 iterations for PBKDF2-SHA256 (OWASP 2023 recommendation).",
|
|
103
|
-
"Prefer bcrypt (cost ≥ 12) or Argon2id instead."
|
|
104
|
-
]
|
|
105
|
-
});
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
116
|
+
const pbkdf2Finding = checkPbkdf2Iterations(pbkdf2Hits);
|
|
117
|
+
if (pbkdf2Finding)
|
|
118
|
+
findings.push(pbkdf2Finding);
|
|
110
119
|
// 6. Hardcoded IV/nonce
|
|
111
120
|
const hardcodedIvHits = await searchRepo({
|
|
112
121
|
query: String.raw `iv\s*[:=]\s*(?:Buffer\.from\(['"][0-9a-fA-F]+['"]\)|['"][0-9a-fA-F]{16,}['"])`,
|
|
@@ -145,9 +154,69 @@ export async function checkCrypto(_opts) {
|
|
|
145
154
|
]
|
|
146
155
|
});
|
|
147
156
|
}
|
|
157
|
+
// 8. Post-quantum readiness: RSA-1024
|
|
158
|
+
const rsa1024Hits = await searchRepo({
|
|
159
|
+
query: String.raw `modulusLength\s*:\s*1024|generateKeyPair\s*\(\s*['"]rsa['"][^)]*1024`,
|
|
160
|
+
isRegex: true,
|
|
161
|
+
maxMatches: 200
|
|
162
|
+
});
|
|
163
|
+
if (rsa1024Hits.length > 0) {
|
|
164
|
+
findings.push({
|
|
165
|
+
id: "CRYPTO_RSA_1024",
|
|
166
|
+
title: "RSA-1024 key detected — cryptographically broken",
|
|
167
|
+
severity: "CRITICAL",
|
|
168
|
+
evidence: rsa1024Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
169
|
+
files: [...new Set(rsa1024Hits.slice(0, 10).map((m) => m.file))],
|
|
170
|
+
requiredActions: [
|
|
171
|
+
"Upgrade to RSA-4096 minimum, or migrate to ML-DSA (FIPS 204) / SLH-DSA (FIPS 205) for new key material.",
|
|
172
|
+
"RSA-1024 is fully broken — NIST deprecated it in 2013 (SP 800-131A).",
|
|
173
|
+
"For TLS certificates, reissue with RSA-4096 or ECDSA P-384 immediately."
|
|
174
|
+
]
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// 9. Post-quantum readiness: RSA-2048 warning
|
|
178
|
+
const rsa2048Hits = await searchRepo({
|
|
179
|
+
query: String.raw `modulusLength\s*:\s*2048|generateKeyPair\s*\(\s*['"]rsa['"][^)]*2048`,
|
|
180
|
+
isRegex: true,
|
|
181
|
+
maxMatches: 200
|
|
182
|
+
});
|
|
183
|
+
if (rsa2048Hits.length > 0) {
|
|
184
|
+
findings.push({
|
|
185
|
+
id: "CRYPTO_RSA_2048_PQC",
|
|
186
|
+
title: "RSA-2048 detected — quantum-vulnerable; plan migration to post-quantum algorithms",
|
|
187
|
+
severity: "MEDIUM",
|
|
188
|
+
evidence: rsa2048Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
189
|
+
files: [...new Set(rsa2048Hits.slice(0, 10).map((m) => m.file))],
|
|
190
|
+
requiredActions: [
|
|
191
|
+
"RSA-2048 is currently secure against classical computers but will be broken by sufficiently large quantum computers.",
|
|
192
|
+
"NIST finalized post-quantum standards in 2024: ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205).",
|
|
193
|
+
"For long-lived keys or data requiring 10+ year secrecy: migrate to ML-DSA or use a hybrid classical+PQC scheme."
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// 10. Post-quantum readiness: ECDSA P-256 (informational)
|
|
198
|
+
const p256Hits = await searchRepo({
|
|
199
|
+
query: String.raw `prime256v1|secp256r1|namedCurve\s*:\s*['"]P-256['"]|namedCurve\s*:\s*['"]p256['"]`,
|
|
200
|
+
isRegex: true,
|
|
201
|
+
maxMatches: 200
|
|
202
|
+
});
|
|
203
|
+
if (p256Hits.length > 0) {
|
|
204
|
+
findings.push({
|
|
205
|
+
id: "CRYPTO_ECDSA_P256_PQC",
|
|
206
|
+
title: "ECDSA P-256 detected — quantum-vulnerable in the long term",
|
|
207
|
+
severity: "LOW",
|
|
208
|
+
evidence: p256Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
209
|
+
files: [...new Set(p256Hits.slice(0, 10).map((m) => m.file))],
|
|
210
|
+
requiredActions: [
|
|
211
|
+
"P-256 (secp256r1) is secure today but vulnerable to Shor's algorithm on a sufficiently large quantum computer.",
|
|
212
|
+
"NIST post-quantum signature standards: ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) are the recommended replacements.",
|
|
213
|
+
"For new systems handling sensitive long-lived data, evaluate hybrid ECDSA+ML-DSA or pure ML-DSA."
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
148
217
|
}
|
|
149
218
|
catch (err) {
|
|
150
|
-
console.warn("[checkCrypto] Internal error:", err instanceof Error ? err.message : String(err));
|
|
219
|
+
console.warn("[checkCrypto] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
151
220
|
}
|
|
152
221
|
return findings;
|
|
153
222
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database security checks.
|
|
3
|
+
*/
|
|
4
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
1
5
|
import { searchRepo } from "../../repo/search.js";
|
|
2
6
|
export async function checkDatabase(_opts) {
|
|
3
7
|
const findings = [];
|
|
@@ -138,7 +142,7 @@ export async function checkDatabase(_opts) {
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
catch (err) {
|
|
141
|
-
console.warn("[checkDatabase] Internal error:", err instanceof Error ? err.message : String(err));
|
|
145
|
+
console.warn("[checkDatabase] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
142
146
|
}
|
|
143
147
|
return findings;
|
|
144
148
|
}
|