kramscan 0.1.1 → 0.2.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.
Files changed (72) hide show
  1. package/README.md +392 -236
  2. package/dist/agent/confirmation.d.ts +5 -1
  3. package/dist/agent/confirmation.js +29 -9
  4. package/dist/agent/context.js +2 -3
  5. package/dist/agent/orchestrator.d.ts +2 -0
  6. package/dist/agent/orchestrator.js +50 -8
  7. package/dist/agent/prompts/system.d.ts +1 -1
  8. package/dist/agent/prompts/system.js +5 -7
  9. package/dist/agent/skills/health-check.js +22 -2
  10. package/dist/agent/skills/index.d.ts +1 -0
  11. package/dist/agent/skills/index.js +3 -1
  12. package/dist/agent/skills/verify-finding.d.ts +17 -0
  13. package/dist/agent/skills/verify-finding.js +91 -0
  14. package/dist/agent/skills/web-scan.js +46 -0
  15. package/dist/cli.js +150 -149
  16. package/dist/commands/agent.js +38 -38
  17. package/dist/commands/ai.d.ts +2 -0
  18. package/dist/commands/ai.js +112 -0
  19. package/dist/commands/analyze.js +103 -54
  20. package/dist/commands/config.js +55 -29
  21. package/dist/commands/doctor.js +20 -15
  22. package/dist/commands/onboard.js +188 -141
  23. package/dist/commands/report.js +68 -76
  24. package/dist/commands/scan.js +261 -81
  25. package/dist/commands/scans.d.ts +2 -0
  26. package/dist/commands/scans.js +51 -0
  27. package/dist/core/ai-client.d.ts +6 -1
  28. package/dist/core/ai-client.js +80 -12
  29. package/dist/core/ai-payloads.d.ts +17 -0
  30. package/dist/core/ai-payloads.js +54 -0
  31. package/dist/core/config-schema.d.ts +197 -0
  32. package/dist/core/config-schema.js +68 -0
  33. package/dist/core/config-schema.test.d.ts +1 -0
  34. package/dist/core/config-schema.test.js +151 -0
  35. package/dist/core/config.d.ts +8 -31
  36. package/dist/core/config.js +68 -11
  37. package/dist/core/errors.d.ts +71 -0
  38. package/dist/core/errors.js +162 -0
  39. package/dist/core/scan-index.d.ts +19 -0
  40. package/dist/core/scan-index.js +52 -0
  41. package/dist/core/scan-storage.d.ts +11 -0
  42. package/dist/core/scan-storage.js +69 -0
  43. package/dist/core/scanner.d.ts +95 -13
  44. package/dist/core/scanner.js +336 -248
  45. package/dist/core/vulnerability-detector.d.ts +3 -0
  46. package/dist/core/vulnerability-detector.js +25 -15
  47. package/dist/core/vulnerability-detector.test.d.ts +1 -0
  48. package/dist/core/vulnerability-detector.test.js +210 -0
  49. package/dist/index.js +3 -0
  50. package/dist/plugins/PluginManager.d.ts +27 -0
  51. package/dist/plugins/PluginManager.js +166 -0
  52. package/dist/plugins/index.d.ts +7 -0
  53. package/dist/plugins/index.js +19 -0
  54. package/dist/plugins/types.d.ts +55 -0
  55. package/dist/plugins/types.js +25 -0
  56. package/dist/plugins/vulnerabilities/CSRFPlugin.d.ts +8 -0
  57. package/dist/plugins/vulnerabilities/CSRFPlugin.js +34 -0
  58. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.d.ts +11 -0
  59. package/dist/plugins/vulnerabilities/SQLInjectionPlugin.js +109 -0
  60. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.d.ts +11 -0
  61. package/dist/plugins/vulnerabilities/SecurityHeadersPlugin.js +63 -0
  62. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.d.ts +9 -0
  63. package/dist/plugins/vulnerabilities/SensitiveDataPlugin.js +32 -0
  64. package/dist/plugins/vulnerabilities/XSSPlugin.d.ts +15 -0
  65. package/dist/plugins/vulnerabilities/XSSPlugin.js +81 -0
  66. package/dist/reports/PdfGenerator.d.ts +36 -0
  67. package/dist/reports/PdfGenerator.js +379 -0
  68. package/dist/utils/logger.d.ts +33 -1
  69. package/dist/utils/logger.js +127 -8
  70. package/dist/utils/theme.d.ts +55 -0
  71. package/dist/utils/theme.js +195 -0
  72. package/package.json +1 -1
@@ -1,47 +1,66 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.registerAnalyzeCommand = registerAnalyzeCommand;
40
+ const commander_1 = require("commander");
7
41
  const chalk_1 = __importDefault(require("chalk"));
8
42
  const ai_client_1 = require("../core/ai-client");
43
+ const scan_storage_1 = require("../core/scan-storage");
9
44
  const logger_1 = require("../utils/logger");
45
+ const theme_1 = require("../utils/theme");
10
46
  const promises_1 = __importDefault(require("fs/promises"));
11
- const path_1 = __importDefault(require("path"));
12
- const os_1 = __importDefault(require("os"));
13
47
  function registerAnalyzeCommand(program) {
14
48
  program
15
49
  .command("analyze [scan-file]")
16
50
  .description("AI-powered analysis of scan results")
17
51
  .option("-m, --model <name>", "Override default AI model")
18
52
  .option("-v, --verbose", "Show detailed analysis")
19
- .action(async (scanFile, options) => {
53
+ .action(async (scanFile) => {
20
54
  console.log("");
21
- console.log(chalk_1.default.bold.cyan("🧠 AI Security Analysis"));
22
- console.log(chalk_1.default.gray("".repeat(50)));
55
+ console.log(chalk_1.default.bold.cyan("AI Security Analysis"));
56
+ console.log(chalk_1.default.gray("-".repeat(50)));
23
57
  console.log("");
58
+ let spinner = null;
24
59
  try {
25
- // Load scan results
26
- let filepath;
27
- if (scanFile) {
28
- filepath = path_1.default.isAbsolute(scanFile)
29
- ? scanFile
30
- : path_1.default.join(process.cwd(), scanFile);
31
- }
32
- else {
33
- // Find latest scan
34
- const scanDir = path_1.default.join(os_1.default.homedir(), ".kramscan", "scans");
35
- const files = await promises_1.default.readdir(scanDir);
36
- const scanFiles = files.filter((f) => f.endsWith(".json"));
37
- if (scanFiles.length === 0) {
38
- logger_1.logger.error("No scan results found. Run 'kramscan scan <url>' first.");
39
- process.exit(1);
40
- }
41
- // Get most recent
42
- scanFiles.sort().reverse();
43
- filepath = path_1.default.join(scanDir, scanFiles[0]);
44
- logger_1.logger.info(`Using latest scan: ${scanFiles[0]}`);
60
+ const resolved = await (0, scan_storage_1.resolveScanFile)(scanFile);
61
+ const filepath = resolved.filepath;
62
+ if (resolved.isLatest) {
63
+ logger_1.logger.info(`Using latest scan: ${resolved.filename}`);
45
64
  }
46
65
  const content = await promises_1.default.readFile(filepath, "utf-8");
47
66
  const scanResult = JSON.parse(content);
@@ -49,26 +68,53 @@ function registerAnalyzeCommand(program) {
49
68
  logger_1.logger.success("No vulnerabilities to analyze!");
50
69
  return;
51
70
  }
52
- const spinner = logger_1.logger.spinner("Analyzing vulnerabilities with AI...");
53
- // Create AI client
71
+ spinner = logger_1.logger.spinner("Analyzing vulnerabilities with AI...");
72
+ const { getConfig } = await Promise.resolve().then(() => __importStar(require("../core/config")));
73
+ const config = await getConfig();
74
+ if (!config.ai.enabled) {
75
+ spinner.stop();
76
+ const { default: inquirer } = await Promise.resolve().then(() => __importStar(require("inquirer")));
77
+ const { setup } = await inquirer.prompt([
78
+ {
79
+ type: "confirm",
80
+ name: "setup",
81
+ message: theme_1.theme.yellow("AI analysis is not enabled. Would you like to set it up now?"),
82
+ default: true,
83
+ },
84
+ ]);
85
+ if (setup) {
86
+ const { registerOnboardCommand } = await Promise.resolve().then(() => __importStar(require("./onboard")));
87
+ const onboardProgram = new commander_1.Command();
88
+ registerOnboardCommand(onboardProgram);
89
+ await onboardProgram.parseAsync(["node", "kramscan", "onboard"]);
90
+ // Re-check after onboarding
91
+ const updatedConfig = await getConfig();
92
+ if (!updatedConfig.ai.enabled) {
93
+ logger_1.logger.error("AI analysis still not enabled. Exiting.");
94
+ return;
95
+ }
96
+ spinner.start("Analyzing vulnerabilities with AI...");
97
+ }
98
+ else {
99
+ logger_1.logger.warn("AI analysis skipped.");
100
+ return;
101
+ }
102
+ }
54
103
  const aiClient = await (0, ai_client_1.createAIClient)();
55
- // Build analysis prompt
56
104
  const prompt = buildAnalysisPrompt(scanResult);
57
- // Get AI analysis
58
105
  const response = await aiClient.analyze(prompt);
59
106
  spinner.succeed("Analysis complete!");
60
107
  console.log("");
61
- console.log(chalk_1.default.bold("📝 AI Analysis"));
62
- console.log(chalk_1.default.gray("".repeat(50)));
108
+ console.log(chalk_1.default.bold("AI Analysis"));
109
+ console.log(chalk_1.default.gray("-".repeat(50)));
63
110
  console.log("");
64
111
  console.log(response.content);
65
112
  console.log("");
66
113
  if (response.usage) {
67
- console.log(chalk_1.default.gray("".repeat(50)));
114
+ console.log(chalk_1.default.gray("-".repeat(50)));
68
115
  console.log(chalk_1.default.gray(`Tokens used: ${response.usage.totalTokens} (${response.usage.promptTokens} prompt + ${response.usage.completionTokens} completion)`));
69
116
  console.log("");
70
117
  }
71
- // Save enhanced results
72
118
  const enhancedResult = {
73
119
  ...scanResult,
74
120
  aiAnalysis: {
@@ -82,6 +128,9 @@ function registerAnalyzeCommand(program) {
82
128
  console.log("");
83
129
  }
84
130
  catch (error) {
131
+ if (spinner) {
132
+ spinner.fail("Analysis failed");
133
+ }
85
134
  logger_1.logger.error(error.message);
86
135
  process.exit(1);
87
136
  }
@@ -89,27 +138,27 @@ function registerAnalyzeCommand(program) {
89
138
  }
90
139
  function buildAnalysisPrompt(scanResult) {
91
140
  const vulnList = scanResult.vulnerabilities
92
- .map((v, i) => `${i + 1}. [${v.severity.toUpperCase()}] ${v.title}
93
- URL: ${v.url}
94
- Description: ${v.description}
95
- ${v.evidence ? `Evidence: ${v.evidence}` : ""}
96
- ${v.cwe ? `CWE: ${v.cwe}` : ""}`)
141
+ .map((v, i) => `${i + 1}. [${v.severity.toUpperCase()}] ${v.title}\n` +
142
+ ` URL: ${v.url}\n` +
143
+ ` Description: ${v.description}\n` +
144
+ `${v.evidence ? ` Evidence: ${v.evidence}\n` : ""}` +
145
+ `${v.cwe ? ` CWE: ${v.cwe}` : ""}`)
97
146
  .join("\n\n");
98
- return `You are a security expert analyzing web application vulnerabilities.
99
-
100
- Target: ${scanResult.target}
101
- Scan Date: ${scanResult.timestamp}
102
- Total Vulnerabilities: ${scanResult.summary.total}
103
-
104
- Vulnerabilities Found:
105
- ${vulnList}
106
-
107
- Please provide:
108
- 1. **Executive Summary**: Brief overview of the security posture
109
- 2. **Risk Assessment**: Overall risk level and business impact
110
- 3. **Priority Recommendations**: Top 3-5 vulnerabilities to fix first, with specific remediation steps
111
- 4. **Attack Scenarios**: How an attacker could chain these vulnerabilities
112
- 5. **Remediation Roadmap**: Step-by-step plan to address all findings
113
-
147
+ return `You are a security expert analyzing web application vulnerabilities.
148
+
149
+ Target: ${scanResult.target}
150
+ Scan Date: ${scanResult.timestamp}
151
+ Total Vulnerabilities: ${scanResult.summary.total}
152
+
153
+ Vulnerabilities Found:
154
+ ${vulnList}
155
+
156
+ Please provide:
157
+ 1. Executive Summary: Brief overview of the security posture
158
+ 2. Risk Assessment: Overall risk level and business impact
159
+ 3. Priority Recommendations: Top 3-5 vulnerabilities to fix first, with specific remediation steps
160
+ 4. Attack Scenarios: How an attacker could chain these vulnerabilities
161
+ 5. Remediation Roadmap: Step-by-step plan to address all findings
162
+
114
163
  Format your response in clear markdown with headers and bullet points.`;
115
164
  }
@@ -22,21 +22,26 @@ function registerConfigCommand(program) {
22
22
  logger_1.logger.error(`Configuration key '${key}' not found`);
23
23
  process.exit(1);
24
24
  }
25
- console.log(chalk_1.default.cyan(key), "=", chalk_1.default.white(JSON.stringify(value, null, 2)));
25
+ const displayValue = key === "ai.apiKey" && typeof value === "string" && value
26
+ ? "***configured***"
27
+ : value;
28
+ console.log(chalk_1.default.cyan(key), "=", chalk_1.default.white(JSON.stringify(displayValue, null, 2)));
26
29
  });
27
30
  configCmd
28
31
  .command("set <key> <value>")
29
32
  .description("Set a configuration value")
30
33
  .action(async (key, value) => {
31
34
  const config = await (0, config_1.getConfig)();
32
- // Parse value
33
35
  let parsedValue = value;
34
- if (value === "true")
36
+ if (value === "true") {
35
37
  parsedValue = true;
36
- else if (value === "false")
38
+ }
39
+ else if (value === "false") {
37
40
  parsedValue = false;
38
- else if (!isNaN(Number(value)))
41
+ }
42
+ else if (!Number.isNaN(Number(value))) {
39
43
  parsedValue = Number(value);
44
+ }
40
45
  setNestedValue(config, key, parsedValue);
41
46
  await (0, config_1.setConfig)(config);
42
47
  logger_1.logger.success(`Set ${chalk_1.default.cyan(key)} = ${chalk_1.default.white(JSON.stringify(parsedValue))}`);
@@ -46,11 +51,18 @@ function registerConfigCommand(program) {
46
51
  .description("List all configuration")
47
52
  .action(async () => {
48
53
  const config = await (0, config_1.getConfig)();
54
+ const sanitizedConfig = {
55
+ ...config,
56
+ ai: {
57
+ ...config.ai,
58
+ apiKey: config.ai.apiKey ? "***configured***" : "",
59
+ },
60
+ };
49
61
  console.log("");
50
- console.log(chalk_1.default.bold.cyan("📋 Current Configuration"));
51
- console.log(chalk_1.default.gray("".repeat(50)));
62
+ console.log(chalk_1.default.bold.cyan("Current Configuration"));
63
+ console.log(chalk_1.default.gray("-".repeat(50)));
52
64
  console.log("");
53
- console.log(JSON.stringify(config, null, 2));
65
+ console.log(JSON.stringify(sanitizedConfig, null, 2));
54
66
  console.log("");
55
67
  });
56
68
  configCmd
@@ -59,8 +71,8 @@ function registerConfigCommand(program) {
59
71
  .action(async () => {
60
72
  const config = await (0, config_1.getConfig)();
61
73
  console.log("");
62
- console.log(chalk_1.default.bold.cyan("⚙️ Configuration Editor"));
63
- console.log(chalk_1.default.gray("".repeat(50)));
74
+ console.log(chalk_1.default.bold.cyan("Configuration Editor"));
75
+ console.log(chalk_1.default.gray("-".repeat(50)));
64
76
  console.log("");
65
77
  const answers = await inquirer_1.default.prompt([
66
78
  {
@@ -73,23 +85,23 @@ function registerConfigCommand(program) {
73
85
  type: "list",
74
86
  name: "aiProvider",
75
87
  message: "AI provider:",
76
- choices: ["openai", "anthropic"],
88
+ choices: ["openai", "anthropic", "gemini", "openrouter", "mistral", "kimi", "groq"],
77
89
  default: config.ai.provider,
78
- when: (answers) => answers.aiEnabled,
90
+ when: (ans) => ans.aiEnabled,
79
91
  },
80
92
  {
81
93
  type: "input",
82
94
  name: "apiKey",
83
- message: "API key:",
84
- default: config.ai.apiKey,
85
- when: (answers) => answers.aiEnabled,
95
+ message: "API key (leave blank to keep current):",
96
+ default: "",
97
+ when: (ans) => ans.aiEnabled,
86
98
  },
87
99
  {
88
100
  type: "input",
89
101
  name: "model",
90
102
  message: "Default AI model:",
91
103
  default: config.ai.defaultModel,
92
- when: (answers) => answers.aiEnabled,
104
+ when: (ans) => ans.aiEnabled,
93
105
  },
94
106
  {
95
107
  type: "list",
@@ -103,36 +115,50 @@ function registerConfigCommand(program) {
103
115
  name: "rateLimit",
104
116
  message: "Requests per second rate limit:",
105
117
  default: config.scan.rateLimitPerSecond,
118
+ validate: (value) => Number.isFinite(value) && value > 0
119
+ ? true
120
+ : "Rate limit must be a positive number",
106
121
  },
107
122
  ]);
108
- // Update config
109
- if (answers.aiEnabled !== undefined)
123
+ if (answers.aiEnabled !== undefined) {
110
124
  config.ai.enabled = answers.aiEnabled;
111
- if (answers.aiProvider)
125
+ }
126
+ if (answers.aiProvider) {
112
127
  config.ai.provider = answers.aiProvider;
113
- if (answers.apiKey)
128
+ }
129
+ if (answers.apiKey) {
114
130
  config.ai.apiKey = answers.apiKey;
115
- if (answers.model)
131
+ }
132
+ if (answers.model) {
116
133
  config.ai.defaultModel = answers.model;
117
- if (answers.reportFormat)
134
+ }
135
+ if (answers.reportFormat) {
118
136
  config.report.defaultFormat = answers.reportFormat;
119
- if (answers.rateLimit)
137
+ }
138
+ if (Number.isFinite(answers.rateLimit) && answers.rateLimit > 0) {
120
139
  config.scan.rateLimitPerSecond = answers.rateLimit;
121
- (0, config_1.setConfig)(config);
140
+ }
141
+ await (0, config_1.setConfig)(config);
122
142
  console.log("");
123
143
  logger_1.logger.success("Configuration updated successfully!");
124
144
  console.log("");
125
145
  });
126
146
  }
127
- function getNestedValue(obj, path) {
128
- return path.split(".").reduce((current, key) => current?.[key], obj);
147
+ function getNestedValue(obj, keyPath) {
148
+ return keyPath
149
+ .split(".")
150
+ .reduce((current, key) => current?.[key], obj);
129
151
  }
130
- function setNestedValue(obj, path, value) {
131
- const keys = path.split(".");
152
+ function setNestedValue(obj, keyPath, value) {
153
+ const keys = keyPath.split(".");
132
154
  const lastKey = keys.pop();
155
+ if (!lastKey) {
156
+ return;
157
+ }
133
158
  const target = keys.reduce((current, key) => {
134
- if (!current[key])
159
+ if (!current[key] || typeof current[key] !== "object") {
135
160
  current[key] = {};
161
+ }
136
162
  return current[key];
137
163
  }, obj);
138
164
  target[lastKey] = value;
@@ -40,11 +40,9 @@ exports.registerDoctorCommand = registerDoctorCommand;
40
40
  const chalk_1 = __importDefault(require("chalk"));
41
41
  const config_1 = require("../core/config");
42
42
  const logger_1 = require("../utils/logger");
43
- const child_process_1 = require("child_process");
44
- const util_1 = require("util");
43
+ const dns = __importStar(require("dns/promises"));
45
44
  const promises_1 = __importDefault(require("fs/promises"));
46
45
  const os_1 = __importDefault(require("os"));
47
- const execAsync = (0, util_1.promisify)(child_process_1.exec);
48
46
  function registerDoctorCommand(program) {
49
47
  program
50
48
  .command("doctor")
@@ -123,7 +121,7 @@ async function checkNodeVersion() {
123
121
  }
124
122
  async function checkPuppeteer() {
125
123
  try {
126
- const puppeteer = await Promise.resolve().then(() => __importStar(require("puppeteer")));
124
+ await Promise.resolve().then(() => __importStar(require("puppeteer")));
127
125
  return {
128
126
  name: "Puppeteer",
129
127
  status: "pass",
@@ -158,6 +156,15 @@ async function checkConfig() {
158
156
  async function checkAPIKeys() {
159
157
  try {
160
158
  const config = await (0, config_1.getConfig)();
159
+ const envFallback = {
160
+ openai: process.env.OPENAI_API_KEY,
161
+ anthropic: process.env.ANTHROPIC_API_KEY,
162
+ gemini: process.env.GEMINI_API_KEY,
163
+ openrouter: process.env.OPENROUTER_API_KEY,
164
+ mistral: process.env.MISTRAL_API_KEY,
165
+ kimi: process.env.KIMI_API_KEY,
166
+ groq: process.env.GROQ_API_KEY,
167
+ };
161
168
  if (!config.ai.enabled) {
162
169
  return {
163
170
  name: "AI Configuration",
@@ -165,7 +172,8 @@ async function checkAPIKeys() {
165
172
  message: "AI analysis is disabled. Run 'kramscan onboard' to enable it.",
166
173
  };
167
174
  }
168
- if (!config.ai.apiKey) {
175
+ const apiKey = config.ai.apiKey || envFallback[config.ai.provider] || "";
176
+ if (!apiKey) {
169
177
  return {
170
178
  name: "AI Configuration",
171
179
  status: "warn",
@@ -208,16 +216,13 @@ async function checkDiskSpace() {
208
216
  }
209
217
  async function checkNetwork() {
210
218
  try {
211
- // Simple DNS check
212
- const { exec } = require("child_process");
213
- await new Promise((resolve, reject) => {
214
- exec("ping -n 1 8.8.8.8", (error) => {
215
- if (error)
216
- reject(error);
217
- else
218
- resolve(true);
219
- });
220
- });
219
+ const timeoutMs = 3000;
220
+ await Promise.race([
221
+ dns.lookup("example.com"),
222
+ new Promise((_, reject) => {
223
+ setTimeout(() => reject(new Error("DNS lookup timeout")), timeoutMs);
224
+ }),
225
+ ]);
221
226
  return {
222
227
  name: "Network Connectivity",
223
228
  status: "pass",