kramscan 0.3.0 → 0.4.0
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 +215 -293
- package/dist/agent/skills/generate-report.js +1 -1
- package/dist/agent/skills/health-check.d.ts +2 -2
- package/dist/agent/skills/health-check.js +2 -2
- package/dist/agent/skills/verify-finding.js +1 -1
- package/dist/agent/skills/web-scan.d.ts +1 -1
- package/dist/agent/skills/web-scan.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/commands/config.js +2 -2
- package/dist/commands/dev.js +4 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/gate.js +3 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +191 -0
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/report.js +89 -11
- package/dist/commands/scan.js +10 -5
- package/dist/core/config.js +8 -1
- package/dist/core/project-config.d.ts +59 -0
- package/dist/core/project-config.js +104 -0
- package/dist/core/server-probe.js +1 -3
- package/dist/index.js +14 -0
- package/dist/plugins/vulnerabilities/CORSAnalyzerPlugin.js +0 -1
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/DebugEndpointPlugin.js +1 -1
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/DirectoryTraversalPlugin.js +1 -1
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.d.ts +1 -1
- package/dist/plugins/vulnerabilities/OpenRedirectPlugin.js +1 -1
- package/dist/reports/PdfGenerator.js +1 -0
- package/dist/utils/logger.js +3 -3
- package/package.json +3 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Project-level configuration (.kramscanrc)
|
|
4
|
+
*
|
|
5
|
+
* Discovers and loads a .kramscanrc file from the current working directory
|
|
6
|
+
* (or any parent directory) and merges it with the global config. Project
|
|
7
|
+
* settings override global settings, but sensitive fields (ai.apiKey) are
|
|
8
|
+
* never read from the project file.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.PROJECT_CONFIG_FILENAME = void 0;
|
|
45
|
+
exports.findProjectConfig = findProjectConfig;
|
|
46
|
+
exports.deepMerge = deepMerge;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
exports.PROJECT_CONFIG_FILENAME = ".kramscanrc";
|
|
50
|
+
/**
|
|
51
|
+
* Search for a .kramscanrc file starting from `startDir` and walking up
|
|
52
|
+
* to the filesystem root. Returns the parsed contents and file path,
|
|
53
|
+
* or null if no file is found.
|
|
54
|
+
*/
|
|
55
|
+
function findProjectConfig(startDir = process.cwd()) {
|
|
56
|
+
for (let dir = path.resolve(startDir);; dir = path.dirname(dir)) {
|
|
57
|
+
const candidate = path.join(dir, exports.PROJECT_CONFIG_FILENAME);
|
|
58
|
+
if (fs.existsSync(candidate)) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = fs.readFileSync(candidate, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
// Strip any ai.apiKey that might have been added by mistake
|
|
63
|
+
const sanitized = parsed;
|
|
64
|
+
if (sanitized.ai && typeof sanitized.ai === "object") {
|
|
65
|
+
delete sanitized.ai.apiKey;
|
|
66
|
+
}
|
|
67
|
+
return { config: parsed, filepath: candidate };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Malformed file — skip it silently
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const parent = path.dirname(dir);
|
|
75
|
+
if (parent === dir)
|
|
76
|
+
break; // reached filesystem root
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Deep merge `source` into `target`, returning a new object. Arrays are
|
|
82
|
+
* replaced, not concatenated. Undefined values in source are skipped.
|
|
83
|
+
*/
|
|
84
|
+
function deepMerge(target, source) {
|
|
85
|
+
const result = { ...target };
|
|
86
|
+
for (const key of Object.keys(source)) {
|
|
87
|
+
const srcVal = source[key];
|
|
88
|
+
const tgtVal = result[key];
|
|
89
|
+
if (srcVal === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
if (srcVal !== null &&
|
|
92
|
+
typeof srcVal === "object" &&
|
|
93
|
+
!Array.isArray(srcVal) &&
|
|
94
|
+
tgtVal !== null &&
|
|
95
|
+
typeof tgtVal === "object" &&
|
|
96
|
+
!Array.isArray(tgtVal)) {
|
|
97
|
+
result[key] = deepMerge(tgtVal, srcVal);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
result[key] = srcVal;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
@@ -15,7 +15,6 @@ async function probeServer(url, options = {}) {
|
|
|
15
15
|
const { timeout = 30000, interval = 1000, maxAttempts = 20 } = options;
|
|
16
16
|
const startTime = Date.now();
|
|
17
17
|
let attempts = 0;
|
|
18
|
-
let lastError = null;
|
|
19
18
|
while (attempts < maxAttempts && Date.now() - startTime < timeout) {
|
|
20
19
|
attempts++;
|
|
21
20
|
try {
|
|
@@ -23,10 +22,9 @@ async function probeServer(url, options = {}) {
|
|
|
23
22
|
if (result.reachable) {
|
|
24
23
|
return result;
|
|
25
24
|
}
|
|
26
|
-
lastError = `HTTP ${result.statusCode}`;
|
|
27
25
|
}
|
|
28
26
|
catch (err) {
|
|
29
|
-
|
|
27
|
+
// error logged silently
|
|
30
28
|
}
|
|
31
29
|
// Exponential backoff with cap
|
|
32
30
|
const delay = Math.min(interval * Math.pow(1.5, attempts - 1), 5000);
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const cli_1 = require("./cli");
|
|
4
7
|
const errors_1 = require("./core/errors");
|
|
8
|
+
const update_notifier_1 = __importDefault(require("update-notifier"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
5
11
|
// Ensure uncaught exceptions and unhandled rejections produce useful output
|
|
6
12
|
(0, errors_1.setupGlobalErrorHandlers)();
|
|
13
|
+
try {
|
|
14
|
+
const pkgPath = path_1.default.join(__dirname, "../package.json");
|
|
15
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
16
|
+
(0, update_notifier_1.default)({ pkg }).notify({ isGlobal: true });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// Silently ignore if update notification fails
|
|
20
|
+
}
|
|
7
21
|
(0, cli_1.run)().catch((err) => {
|
|
8
22
|
console.error("Fatal error:", err);
|
|
9
23
|
process.exit(1);
|
|
@@ -17,7 +17,6 @@ class CORSAnalyzerPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
|
17
17
|
const acao = headers["access-control-allow-origin"];
|
|
18
18
|
const acac = headers["access-control-allow-credentials"];
|
|
19
19
|
const acam = headers["access-control-allow-methods"];
|
|
20
|
-
const acah = headers["access-control-allow-headers"];
|
|
21
20
|
// Check for wildcard origin
|
|
22
21
|
if (acao === "*") {
|
|
23
22
|
vulnerabilities.push(this.createVulnerability("CORS: Wildcard Origin Allowed", `The server at ${host} allows requests from any origin (Access-Control-Allow-Origin: *). ` +
|
|
@@ -10,6 +10,6 @@ export declare class DebugEndpointPlugin extends BaseVulnerabilityPlugin {
|
|
|
10
10
|
* Each entry has a path, a human-readable name, and expected severity.
|
|
11
11
|
*/
|
|
12
12
|
private readonly debugEndpoints;
|
|
13
|
-
analyzeContent(context: PluginContext,
|
|
13
|
+
analyzeContent(context: PluginContext, _content: string): Promise<Vulnerability[]>;
|
|
14
14
|
reset(): void;
|
|
15
15
|
}
|
|
@@ -174,7 +174,7 @@ class DebugEndpointPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
|
174
174
|
remediation: "Protect /metrics endpoint behind authentication or restrict to internal networks.",
|
|
175
175
|
},
|
|
176
176
|
];
|
|
177
|
-
async analyzeContent(context,
|
|
177
|
+
async analyzeContent(context, _content) {
|
|
178
178
|
const vulnerabilities = [];
|
|
179
179
|
const baseUrl = new URL(context.url).origin;
|
|
180
180
|
for (const endpoint of this.debugEndpoints) {
|
|
@@ -8,6 +8,6 @@ export declare class DirectoryTraversalPlugin extends BaseVulnerabilityPlugin {
|
|
|
8
8
|
* Each payload targets a well-known file with distinctive content markers.
|
|
9
9
|
*/
|
|
10
10
|
private readonly payloads;
|
|
11
|
-
testParameter(context: PluginContext, param: string,
|
|
11
|
+
testParameter(context: PluginContext, param: string, _value: string): Promise<VulnerabilityTestResult>;
|
|
12
12
|
private buildTestUrl;
|
|
13
13
|
}
|
|
@@ -66,7 +66,7 @@ class DirectoryTraversalPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
|
66
66
|
os: "linux",
|
|
67
67
|
},
|
|
68
68
|
];
|
|
69
|
-
async testParameter(context, param,
|
|
69
|
+
async testParameter(context, param, _value) {
|
|
70
70
|
for (const { payload, markers, os } of this.payloads) {
|
|
71
71
|
try {
|
|
72
72
|
const testUrl = this.buildTestUrl(context.url, param, payload);
|
|
@@ -6,5 +6,5 @@ export declare class OpenRedirectPlugin extends BaseVulnerabilityPlugin {
|
|
|
6
6
|
readonly description = "Detects open redirect vulnerabilities in URL parameters";
|
|
7
7
|
private readonly redirectParams;
|
|
8
8
|
private readonly testDomains;
|
|
9
|
-
analyzeContent(context: PluginContext,
|
|
9
|
+
analyzeContent(context: PluginContext, _content: string): Promise<Vulnerability[]>;
|
|
10
10
|
}
|
|
@@ -19,7 +19,7 @@ class OpenRedirectPlugin extends types_1.BaseVulnerabilityPlugin {
|
|
|
19
19
|
"/\\evil.com",
|
|
20
20
|
"https:evil.com",
|
|
21
21
|
];
|
|
22
|
-
async analyzeContent(context,
|
|
22
|
+
async analyzeContent(context, _content) {
|
|
23
23
|
const vulnerabilities = [];
|
|
24
24
|
const url = new URL(context.url);
|
|
25
25
|
// Check if the current URL uses any redirect-like parameters
|
package/dist/utils/logger.js
CHANGED
|
@@ -55,10 +55,10 @@ function outputLog(entry) {
|
|
|
55
55
|
console.log(JSON.stringify(entry));
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
const {
|
|
58
|
+
const { message, context: ctx } = entry;
|
|
59
59
|
let output = message;
|
|
60
|
-
if (process.env.LOG_INCLUDE_CONTEXT === "true" && Object.keys(
|
|
61
|
-
output += ` ${JSON.stringify(
|
|
60
|
+
if (process.env.LOG_INCLUDE_CONTEXT === "true" && ctx && Object.keys(ctx).length > 0) {
|
|
61
|
+
output += ` ${JSON.stringify(ctx)}`;
|
|
62
62
|
}
|
|
63
63
|
console.log(output);
|
|
64
64
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kramscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "KramScan CLI — AI-powered web app security testing",
|
|
5
5
|
"author": "Akram Shaikh (https://akramshaikh.me)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.31.0",
|
|
55
55
|
"@google/generative-ai": "^0.24.1",
|
|
56
56
|
"@mistralai/mistralai": "^1.14.0",
|
|
57
|
+
"@types/update-notifier": "^5.1.0",
|
|
57
58
|
"axios": "^1.6.8",
|
|
58
59
|
"chalk": "^5.6.2",
|
|
59
60
|
"commander": "^12.1.0",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"openai": "^4.104.0",
|
|
66
67
|
"ora": "^8.2.0",
|
|
67
68
|
"puppeteer": "^22.15.0",
|
|
69
|
+
"update-notifier": "^5.1.0",
|
|
68
70
|
"uuid": "^9.0.1"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|