prodlint 0.9.1 → 0.9.3

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/dist/index.d.ts CHANGED
@@ -50,7 +50,7 @@ interface CategoryScore {
50
50
  score: number;
51
51
  findingCount: number;
52
52
  }
53
- interface ScanResult$1 {
53
+ interface ScanResult {
54
54
  version: string;
55
55
  scannedPath: string;
56
56
  filesScanned: number;
@@ -69,20 +69,26 @@ interface ScanOptions {
69
69
  ignore?: string[];
70
70
  }
71
71
 
72
- declare function scan(options: ScanOptions): Promise<ScanResult$1>;
72
+ declare function scan(options: ScanOptions): Promise<ScanResult>;
73
73
 
74
74
  declare const rules: Rule[];
75
75
 
76
+ interface ReportOptions {
77
+ quiet?: boolean;
78
+ }
79
+ declare function reportPretty(result: ScanResult, opts?: ReportOptions): string;
80
+ declare function reportJson(result: ScanResult): string;
81
+ declare function reportSummary(result: ScanResult): string;
82
+ declare function reportSarif(result: ScanResult): string;
83
+
76
84
  type CheckStatus = "pass" | "fail" | "warn" | "info";
77
85
  type CheckSeverity = "critical" | "high" | "medium" | "low";
78
- type CheckMaturity = "established" | "emerging";
79
86
  interface ScanCheck {
80
87
  id: string;
81
88
  name: string;
82
89
  description: string;
83
90
  status: CheckStatus;
84
91
  severity: CheckSeverity;
85
- maturity: CheckMaturity;
86
92
  points: number;
87
93
  maxPoints: number;
88
94
  details?: string;
@@ -101,7 +107,7 @@ interface CheckContext {
101
107
  html: string | null;
102
108
  loadTimeMs: number | null;
103
109
  }
104
- interface ScanResult {
110
+ interface WebScanResult {
105
111
  url: string;
106
112
  domain: string;
107
113
  scannedAt: string;
@@ -116,6 +122,6 @@ interface ScanResult {
116
122
  };
117
123
  }
118
124
 
119
- declare function runWebScan(targetUrl: string): Promise<ScanResult>;
125
+ declare function runWebScan(targetUrl: string): Promise<WebScanResult>;
120
126
 
121
- export { type Category, type CategoryScore, type FileContext, type Finding, type ProjectContext, type Rule, type ScanOptions, type ScanResult$1 as ScanResult, type Severity, type CheckContext as WebCheckContext, type ScanCheck as WebScanCheck, type ScanResult as WebScanResult, rules, runWebScan, scan };
127
+ export { type Category, type CategoryScore, type FileContext, type Finding, type ProjectContext, type Rule, type ScanOptions, type ScanResult, type Severity, type CheckContext as WebCheckContext, type ScanCheck as WebScanCheck, type WebScanResult, reportJson, reportPretty, reportSarif, reportSummary, rules, runWebScan, scan };
package/dist/index.js CHANGED
@@ -4501,114 +4501,305 @@ async function scan(options) {
4501
4501
  };
4502
4502
  }
4503
4503
 
4504
+ // src/reporter.ts
4505
+ import pc from "picocolors";
4506
+ var SEVERITY_COLORS = {
4507
+ critical: pc.red,
4508
+ warning: pc.yellow,
4509
+ info: pc.blue
4510
+ };
4511
+ var SEVERITY_LABELS = {
4512
+ critical: "CRIT",
4513
+ warning: "WARN",
4514
+ info: "INFO"
4515
+ };
4516
+ function scoreColor(score) {
4517
+ if (score >= 80) return pc.green;
4518
+ if (score >= 50) return pc.yellow;
4519
+ return pc.red;
4520
+ }
4521
+ function groupByFile(findings) {
4522
+ const map = /* @__PURE__ */ new Map();
4523
+ for (const f of findings) {
4524
+ const group = map.get(f.file) ?? [];
4525
+ group.push(f);
4526
+ map.set(f.file, group);
4527
+ }
4528
+ return map;
4529
+ }
4530
+ function reportPretty(result, opts = {}) {
4531
+ const lines = [];
4532
+ const { critical, warning, info } = result.summary;
4533
+ lines.push("");
4534
+ lines.push(pc.bold(" prodlint") + pc.dim(` v${result.version}`));
4535
+ const headerParts = [`Scanned ${result.filesScanned} files`];
4536
+ if (critical > 0) headerParts.push(`${critical} critical`);
4537
+ if (warning > 0) headerParts.push(`${warning} warnings`);
4538
+ if (info > 0) headerParts.push(`${info} info`);
4539
+ lines.push(pc.dim(` ${headerParts.join(" \xB7 ")}`));
4540
+ lines.push("");
4541
+ if (result.findings.length > 0) {
4542
+ const grouped = groupByFile(result.findings);
4543
+ for (const [file, findings] of grouped) {
4544
+ lines.push(pc.underline(file));
4545
+ for (const f of findings) {
4546
+ const color = SEVERITY_COLORS[f.severity];
4547
+ const label = SEVERITY_LABELS[f.severity];
4548
+ lines.push(
4549
+ ` ${pc.dim(`${f.line}:${f.column}`)} ${color(label)} ${f.message} ${pc.dim(f.ruleId)}`
4550
+ );
4551
+ if (f.fix) {
4552
+ lines.push(` ${pc.dim(` \u21B3 ${f.fix}`)}`);
4553
+ }
4554
+ }
4555
+ lines.push("");
4556
+ }
4557
+ }
4558
+ lines.push(pc.bold(" Scores"));
4559
+ for (const cs of result.categoryScores) {
4560
+ const color = scoreColor(cs.score);
4561
+ const bar = renderBar(cs.score);
4562
+ lines.push(` ${cs.category.padEnd(14)} ${color(String(cs.score).padStart(3))} ${bar} ${pc.dim(`(${cs.findingCount} issues)`)}`);
4563
+ }
4564
+ lines.push("");
4565
+ const overallColor = scoreColor(result.overallScore);
4566
+ lines.push(pc.bold(` Overall: ${overallColor(String(result.overallScore))}/100`));
4567
+ lines.push("");
4568
+ const summaryParts = [];
4569
+ if (critical > 0) summaryParts.push(pc.red(`${critical} critical`));
4570
+ if (warning > 0) summaryParts.push(pc.yellow(`${warning} warnings`));
4571
+ if (info > 0) summaryParts.push(pc.blue(`${info} info`));
4572
+ if (summaryParts.length === 0) {
4573
+ lines.push(pc.green(" No issues found!"));
4574
+ } else {
4575
+ lines.push(` ${summaryParts.join(pc.dim(" \xB7 "))}`);
4576
+ }
4577
+ lines.push("");
4578
+ if (!opts.quiet) {
4579
+ const badgeColor = result.overallScore >= 80 ? "brightgreen" : result.overallScore >= 60 ? "yellow" : "red";
4580
+ const badgeUrl = `https://img.shields.io/badge/prodlint-${result.overallScore}%2F100-${badgeColor}`;
4581
+ lines.push(pc.dim(" Add to your README:"));
4582
+ lines.push(pc.dim(` [![prodlint](${badgeUrl})](https://prodlint.com)`));
4583
+ lines.push("");
4584
+ }
4585
+ return lines.join("\n");
4586
+ }
4587
+ function reportJson(result) {
4588
+ return JSON.stringify(result, null, 2);
4589
+ }
4590
+ function reportSummary(result) {
4591
+ const lines = [];
4592
+ const { critical, warning, info } = result.summary;
4593
+ const hasCritical = critical > 0;
4594
+ const verdict = hasCritical ? pc.red(pc.bold("FAIL")) : pc.green(pc.bold("PASS"));
4595
+ const scorePart = scoreColor(result.overallScore)(`${result.overallScore}/100`);
4596
+ const countParts = [];
4597
+ if (critical > 0) countParts.push(`${critical} critical`);
4598
+ if (warning > 0) countParts.push(`${warning} warning`);
4599
+ if (info > 0) countParts.push(`${info} info`);
4600
+ lines.push("");
4601
+ if (countParts.length > 0) {
4602
+ lines.push(` ${verdict} ${pc.dim("\u2014")} ${countParts.join(", ")} ${pc.dim(`(score: ${scorePart})`)}`);
4603
+ } else {
4604
+ lines.push(` ${verdict} ${pc.dim("\u2014")} no issues ${pc.dim(`(score: ${scorePart})`)}`);
4605
+ }
4606
+ const topFindings = result.findings.filter((f) => f.severity === "critical" || f.severity === "warning").slice(0, 3);
4607
+ if (topFindings.length > 0) {
4608
+ for (let i = 0; i < topFindings.length; i++) {
4609
+ const f = topFindings[i];
4610
+ const color = SEVERITY_COLORS[f.severity];
4611
+ const label = SEVERITY_LABELS[f.severity];
4612
+ lines.push(` ${pc.dim(`${i + 1}.`)} ${f.file}:${f.line} ${pc.dim("\u2014")} ${color(label)} ${f.message} ${pc.dim(f.ruleId)}`);
4613
+ }
4614
+ }
4615
+ lines.push("");
4616
+ return lines.join("\n");
4617
+ }
4618
+ var SARIF_SEVERITY_MAP = {
4619
+ critical: "error",
4620
+ warning: "warning",
4621
+ info: "note"
4622
+ };
4623
+ function reportSarif(result) {
4624
+ const ruleMap = /* @__PURE__ */ new Map();
4625
+ for (const r of rules) {
4626
+ ruleMap.set(r.id, r);
4627
+ }
4628
+ const usedRuleIds = new Set(result.findings.map((f) => f.ruleId));
4629
+ const sarifRules = [...usedRuleIds].map((id) => {
4630
+ const rule = ruleMap.get(id);
4631
+ return {
4632
+ id,
4633
+ name: rule?.name ?? id,
4634
+ shortDescription: { text: rule?.description ?? id },
4635
+ defaultConfiguration: {
4636
+ level: SARIF_SEVERITY_MAP[rule?.severity ?? "info"] ?? "note"
4637
+ },
4638
+ helpUri: `https://prodlint.com/rules/${id}`
4639
+ };
4640
+ });
4641
+ const sarifResults = result.findings.map((f) => {
4642
+ const sarifResult = {
4643
+ ruleId: f.ruleId,
4644
+ level: SARIF_SEVERITY_MAP[f.severity] ?? "note",
4645
+ message: { text: f.message },
4646
+ locations: [
4647
+ {
4648
+ physicalLocation: {
4649
+ artifactLocation: { uri: f.file.replace(/\\/g, "/") },
4650
+ region: { startLine: f.line, startColumn: f.column }
4651
+ }
4652
+ }
4653
+ ]
4654
+ };
4655
+ if (f.fix) {
4656
+ sarifResult.fixes = [
4657
+ {
4658
+ description: { text: f.fix }
4659
+ }
4660
+ ];
4661
+ }
4662
+ return sarifResult;
4663
+ });
4664
+ const sarif = {
4665
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
4666
+ version: "2.1.0",
4667
+ runs: [
4668
+ {
4669
+ tool: {
4670
+ driver: {
4671
+ name: "prodlint",
4672
+ version: getVersion(),
4673
+ informationUri: "https://prodlint.com",
4674
+ rules: sarifRules
4675
+ }
4676
+ },
4677
+ results: sarifResults
4678
+ }
4679
+ ]
4680
+ };
4681
+ return JSON.stringify(sarif, null, 2);
4682
+ }
4683
+ function renderBar(score) {
4684
+ const width = 20;
4685
+ const filled = Math.round(score / 100 * width);
4686
+ const empty = width - filled;
4687
+ const color = scoreColor(score);
4688
+ return color("\u2588".repeat(filled)) + pc.dim("\u2591".repeat(empty));
4689
+ }
4690
+ var STATUS_ICONS = {
4691
+ pass: pc.green,
4692
+ fail: pc.red,
4693
+ warn: pc.yellow,
4694
+ info: pc.blue
4695
+ };
4696
+
4504
4697
  // src/web-scanner/checks.ts
4505
- function make(id, name, description, maxPoints, severity, maturity, status, details) {
4698
+ function make(id, name, description, maxPoints, severity, status, details) {
4506
4699
  return {
4507
4700
  id,
4508
4701
  name,
4509
4702
  description,
4510
4703
  status,
4511
4704
  severity,
4512
- maturity,
4513
4705
  details,
4514
4706
  points: status === "pass" ? maxPoints : status === "warn" ? Math.floor(maxPoints / 2) : 0,
4515
4707
  maxPoints
4516
4708
  };
4517
4709
  }
4518
4710
  function checkRobotsTxt(ctx) {
4519
- if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 7, "medium", "established", "fail", "No robots.txt found.");
4520
- return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 7, "medium", "established", "pass", "robots.txt found.");
4711
+ if (!ctx.robotsTxt) return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "fail", "No robots.txt found.");
4712
+ return make("robots_txt", "robots.txt", "robots.txt exists and is accessible", 5, "medium", "pass", "robots.txt found.");
4521
4713
  }
4522
4714
  function checkRobotsAiDirectives(ctx) {
4523
- if (!ctx.robotsTxt) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "fail", "No robots.txt found. AI bots have no guidance.");
4524
- const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai", "Applebot-Extended", "Grok"];
4715
+ if (!ctx.robotsTxt) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "fail", "No robots.txt found. AI bots have no guidance.");
4716
+ const agents = ["GPTBot", "ChatGPT-User", "ClaudeBot", "Claude-Web", "Google-Extended", "PerplexityBot", "Bytespider", "CCBot", "anthropic-ai", "cohere-ai"];
4525
4717
  const found = agents.filter((a) => ctx.robotsTxt.toLowerCase().includes(a.toLowerCase()));
4526
- if (found.length === 0) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "fail", "No AI-specific user-agent directives found.");
4527
- if (found.length < 3) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "warn", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4528
- return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "established", "pass", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4718
+ if (found.length === 0) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "fail", "No AI-specific user-agent directives found.");
4719
+ if (found.length < 3) return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "warn", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4720
+ return make("robots_ai_directives", "AI robots.txt Directives", "AI bot user-agents in robots.txt", 15, "critical", "pass", `Found ${found.length} AI bot directive(s): ${found.join(", ")}.`);
4529
4721
  }
4530
4722
  function checkContentUsage(ctx) {
4531
4723
  const hasHeader = ctx.headers["content-usage"] != null;
4532
4724
  const hasInRobots = ctx.robotsTxt?.toLowerCase().includes("content-usage") ?? false;
4533
- if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 5, "high", "emerging", "fail", "No Content-Usage directives found.");
4534
- return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 5, "high", "emerging", "pass", hasHeader ? `Header: ${ctx.headers["content-usage"]}` : "Found in robots.txt.");
4725
+ if (!hasHeader && !hasInRobots) return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "fail", "No Content-Usage directives found.");
4726
+ return make("content_usage", "Content-Usage (IETF aipref)", "Content-Usage header or directives", 10, "high", "pass", hasHeader ? `Header: ${ctx.headers["content-usage"]}` : "Found in robots.txt.");
4535
4727
  }
4536
4728
  function checkLlmsTxt(ctx) {
4537
- if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "fail", "No llms.txt found.");
4538
- const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4539
- if (lines.length < 3) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "warn", "llms.txt found but appears minimal.");
4540
- return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 6, "high", "emerging", "pass", `llms.txt found with ${lines.length} content lines.`);
4729
+ if (!ctx.llmsTxt) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "fail", "No llms.txt found.");
4730
+ const lines = ctx.llmsTxt.split("\n").filter((l) => l.trim().length > 0);
4731
+ if (lines.length < 3) return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "warn", "llms.txt found but appears minimal.");
4732
+ return make("llms_txt", "llms.txt", "LLM-optimized site summary at /llms.txt", 10, "high", "pass", `llms.txt found with ${lines.length} lines.`);
4541
4733
  }
4542
4734
  function checkTdmRep(ctx) {
4543
4735
  const hasWK = ctx.tdmRep != null;
4544
4736
  const hasHeader = ctx.headers["tdm-reservation"] != null;
4545
- if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 4, "medium", "emerging", "fail", "No TDMRep configuration found.");
4546
- return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 4, "medium", "emerging", "pass", hasWK ? "/.well-known/tdmrep.json found." : `TDM-Reservation header: ${ctx.headers["tdm-reservation"]}`);
4737
+ if (!hasWK && !hasHeader) return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "fail", "No TDMRep configuration found.");
4738
+ return make("tdmrep", "TDMRep (W3C)", "Text & data mining reservation file or header", 8, "medium", "pass", hasWK ? "/.well-known/tdmrep.json found." : `TDM-Reservation header: ${ctx.headers["tdm-reservation"]}`);
4547
4739
  }
4548
4740
  function checkAiDisclosure(ctx) {
4549
- if (!ctx.headers["ai-disclosure"]) return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 2, "low", "emerging", "fail", "No AI-Disclosure header found.");
4550
- return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 2, "low", "emerging", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
4741
+ if (!ctx.headers["ai-disclosure"]) return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "fail", "No AI-Disclosure header found.");
4742
+ return make("ai_disclosure", "AI-Disclosure Header", "Declares AI involvement in content generation", 5, "low", "pass", `Header: ${ctx.headers["ai-disclosure"]}`);
4551
4743
  }
4552
4744
  function checkAgentCard(ctx) {
4553
- if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "fail", "No A2A AgentCard found.");
4745
+ if (!ctx.agentCard) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "fail", "No A2A AgentCard found.");
4554
4746
  try {
4555
4747
  const card = JSON.parse(ctx.agentCard);
4556
- if (!card.name || !Array.isArray(card.skills) || card.skills.length === 0) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "warn", "AgentCard found but missing name or skills.");
4557
- return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "pass", `AgentCard found with ${card.skills.length} skill(s).`);
4748
+ if (!card.name || !Array.isArray(card.skills) || card.skills.length === 0) return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but missing name or skills.");
4749
+ return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "pass", `AgentCard found with ${card.skills.length} skill(s).`);
4558
4750
  } catch {
4559
- return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 5, "high", "emerging", "warn", "AgentCard found but contains invalid JSON.");
4751
+ return make("agent_card", "A2A AgentCard", "/.well-known/agent-card.json for agent discovery", 10, "high", "warn", "AgentCard found but contains invalid JSON.");
4560
4752
  }
4561
4753
  }
4562
4754
  function checkAiTxt(ctx) {
4563
- if (!ctx.aiTxt) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "fail", "No ai.txt found at site root.");
4755
+ if (!ctx.aiTxt) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "fail", "No ai.txt found at site root.");
4564
4756
  const lines = ctx.aiTxt.split("\n").filter((l) => l.trim().length > 0 && !l.trim().startsWith("#"));
4565
- if (lines.length < 2) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "warn", "ai.txt found but appears minimal.");
4566
- return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 3, "medium", "emerging", "pass", `ai.txt found with ${lines.length} directive(s).`);
4757
+ if (lines.length < 2) return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "warn", "ai.txt found but appears minimal.");
4758
+ return make("ai_txt", "ai.txt", "AI training permissions per Spawning spec", 5, "medium", "pass", `ai.txt found with ${lines.length} directive(s).`);
4567
4759
  }
4568
4760
  function checkWebMCP(ctx) {
4569
- if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "info", "Could not check for WebMCP tools.");
4761
+ if (!ctx.html) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "info", "Could not check for WebMCP tools.");
4570
4762
  const hasToolname = /toolname=/i.test(ctx.html);
4571
4763
  const hasModelContext = /navigator\.modelContext/i.test(ctx.html) || /registerTool/i.test(ctx.html);
4572
- if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "fail", "No WebMCP tools detected.");
4764
+ if (!hasToolname && !hasModelContext) return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "fail", "No WebMCP tools detected.");
4573
4765
  const count = (ctx.html.match(/toolname=/gi) || []).length;
4574
- return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 5, "high", "emerging", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
4766
+ return make("webmcp", "WebMCP Tools", "Chrome 146+ WebMCP tool registration", 10, "high", "pass", hasToolname ? `${count} form(s) with toolname attributes.` : "registerTool() usage detected.");
4575
4767
  }
4576
4768
  function checkStructuredData(ctx) {
4577
- if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "info", "Could not fetch page HTML.");
4769
+ if (!ctx.html) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "info", "Could not fetch page HTML.");
4578
4770
  const jsonLd = ctx.html.match(/<script[^>]*type=["']application\/ld\+json["'][^>]*>/gi);
4579
4771
  const hasMicrodata = ctx.html.includes("itemscope") && ctx.html.includes("itemtype");
4580
- if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "fail", "No structured data found.");
4581
- return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 12, "medium", "established", "pass", `Found ${(jsonLd?.length || 0) + (hasMicrodata ? 1 : 0)} structured data block(s).`);
4772
+ if (!jsonLd && !hasMicrodata) return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "fail", "No structured data found.");
4773
+ return make("structured_data", "Structured Data", "JSON-LD or Schema.org markup", 10, "medium", "pass", `Found ${(jsonLd?.length || 0) + (hasMicrodata ? 1 : 0)} structured data block(s).`);
4582
4774
  }
4583
4775
  function checkOpenGraph(ctx) {
4584
- if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "info", "Could not fetch HTML.");
4776
+ if (!ctx.html) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "info", "Could not fetch HTML.");
4585
4777
  const checks = [/og:title/i.test(ctx.html), /og:description/i.test(ctx.html), /og:image/i.test(ctx.html), /name=["']description["']/i.test(ctx.html)];
4586
4778
  const passed = checks.filter(Boolean).length;
4587
- if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "fail", "No OpenGraph tags or meta description.");
4588
- if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "warn", `Found ${passed}/4 meta tags.`);
4589
- if (passed < 4) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "pass", `Found ${passed}/4 key meta tags.`);
4590
- return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 10, "low", "established", "pass", "All 4 key meta tags present.");
4779
+ if (passed === 0) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "fail", "No OpenGraph tags or meta description.");
4780
+ if (passed < 3) return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "warn", `Found ${passed}/4 meta tags.`);
4781
+ return make("opengraph", "OpenGraph & Meta", "OG tags and meta description", 7, "low", "pass", `All ${passed} key meta tags present.`);
4591
4782
  }
4592
4783
  function checkSitemap(ctx) {
4593
- if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "fail", "No sitemap.xml found.");
4784
+ if (!ctx.sitemapXml) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "fail", "No sitemap.xml found.");
4594
4785
  const count = Math.max((ctx.sitemapXml.match(/<url>/gi) || []).length, (ctx.sitemapXml.match(/<loc>/gi) || []).length);
4595
- if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "pass", "Sitemap index found.");
4596
- if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "warn", "sitemap.xml found but appears empty.");
4597
- return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 10, "medium", "established", "pass", `sitemap.xml found with ${count} URL(s).`);
4786
+ if (count === 0 && /<sitemapindex/i.test(ctx.sitemapXml)) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", "Sitemap index found.");
4787
+ if (count === 0) return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "warn", "sitemap.xml found but appears empty.");
4788
+ return make("sitemap", "sitemap.xml", "Sitemap exists and is valid", 5, "medium", "pass", `sitemap.xml found with ${count} URL(s).`);
4598
4789
  }
4599
4790
  function checkHttpSignatures(ctx) {
4600
4791
  const hasDirectory = ctx.httpSigDirectory != null;
4601
4792
  const hasSignatureHeader = ctx.headers["signature"] != null || ctx.headers["signature-input"] != null;
4602
4793
  const hasSignatureAgent = ctx.headers["signature-agent"] != null;
4603
- if (!hasDirectory && !hasSignatureHeader && !hasSignatureAgent) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "fail", "No HTTP signature support detected.");
4604
- if (hasDirectory) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "pass", "/.well-known/http-message-signatures-directory found.");
4605
- return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 3, "medium", "emerging", "pass", hasSignatureAgent ? "Signature-Agent header detected." : "Signature/Signature-Input headers detected.");
4794
+ if (!hasDirectory && !hasSignatureHeader && !hasSignatureAgent) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "fail", "No HTTP signature support detected.");
4795
+ if (hasDirectory) return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "pass", "/.well-known/http-message-signatures-directory found.");
4796
+ return make("http_signatures", "HTTP Message Signatures (RFC 9421)", "Agent identity verification via cryptographic signatures", 5, "medium", "pass", hasSignatureAgent ? "Signature-Agent header detected." : "Signature/Signature-Input headers detected.");
4606
4797
  }
4607
4798
  function checkPageSpeed(ctx) {
4608
- if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "info", "Could not measure.");
4609
- if (ctx.loadTimeMs > 5e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "fail", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 agents may time out.`);
4610
- if (ctx.loadTimeMs > 2e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "warn", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 consider optimizing.`);
4611
- return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 8, "low", "established", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
4799
+ if (ctx.loadTimeMs == null) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "info", "Could not measure.");
4800
+ if (ctx.loadTimeMs > 5e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "fail", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 agents may time out.`);
4801
+ if (ctx.loadTimeMs > 2e3) return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "warn", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s \u2014 consider optimizing.`);
4802
+ return make("page_speed", "Page Load Time", "Response time for AI agent interactions", 5, "low", "pass", `${(ctx.loadTimeMs / 1e3).toFixed(1)}s.`);
4612
4803
  }
4613
4804
  var allChecks = [
4614
4805
  checkRobotsTxt,
@@ -4647,29 +4838,6 @@ function isPrivateHost(hostname) {
4647
4838
  if (PRIVATE_HOSTNAMES.has(hostname.toLowerCase())) return true;
4648
4839
  const h = hostname.replace(/^\[|\]$/g, "").toLowerCase();
4649
4840
  if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || h === "::") return true;
4650
- const v4mapped = h.match(/^(?:::ffff:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4651
- if (v4mapped) {
4652
- const [, a, b] = v4mapped.map(Number);
4653
- if (a === 10 || a === 127 || a === 0) return true;
4654
- if (a === 172 && b >= 16 && b <= 31) return true;
4655
- if (a === 192 && b === 168) return true;
4656
- if (a === 169 && b === 254) return true;
4657
- return false;
4658
- }
4659
- if (/^(0[xX][0-9a-fA-F]+|0[0-7]+)(\.|$)/.test(hostname)) return true;
4660
- if (/^\d+$/.test(hostname)) {
4661
- const dec = parseInt(hostname, 10);
4662
- if (dec >= 0 && dec <= 4294967295) {
4663
- const a = dec >>> 24 & 255;
4664
- const b = dec >>> 16 & 255;
4665
- if (a === 10 || a === 127 || a === 0) return true;
4666
- if (a === 172 && b >= 16 && b <= 31) return true;
4667
- if (a === 192 && b === 168) return true;
4668
- if (a === 169 && b === 254) return true;
4669
- if (a === 100 && b >= 64 && b <= 127) return true;
4670
- if (a === 198 && (b === 18 || b === 19)) return true;
4671
- }
4672
- }
4673
4841
  const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
4674
4842
  if (ipv4) {
4675
4843
  const [, a, b] = ipv4.map(Number);
@@ -4682,49 +4850,22 @@ function isPrivateHost(hostname) {
4682
4850
  }
4683
4851
  return false;
4684
4852
  }
4685
- function isAllowedProtocol(url) {
4686
- try {
4687
- const p = new URL(url).protocol;
4688
- return p === "https:" || p === "http:";
4689
- } catch {
4690
- return false;
4691
- }
4692
- }
4693
4853
  function validateRedirectUrl(responseUrl) {
4694
4854
  try {
4695
4855
  const parsed = new URL(responseUrl);
4696
- if (!isAllowedProtocol(responseUrl)) return false;
4697
4856
  return !isPrivateHost(parsed.hostname);
4698
4857
  } catch {
4699
4858
  return false;
4700
4859
  }
4701
4860
  }
4702
- var MAX_REDIRECTS = 5;
4703
- async function safeFetch(url, timeout, signal) {
4704
- let current = url;
4705
- for (let i = 0; i < MAX_REDIRECTS; i++) {
4706
- const r = await fetch(current, { signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "manual" });
4707
- const status = r.status;
4708
- if (status >= 300 && status < 400) {
4709
- const location = r.headers.get("location");
4710
- if (!location) return null;
4711
- const resolved = new URL(location, current).toString();
4712
- if (!validateRedirectUrl(resolved)) return null;
4713
- current = resolved;
4714
- continue;
4715
- }
4716
- return r;
4717
- }
4718
- return null;
4719
- }
4720
4861
  async function fetchText(url, timeout = 8e3) {
4721
4862
  try {
4722
4863
  const c = new AbortController();
4723
4864
  const t = setTimeout(() => c.abort(), timeout);
4724
- const r = await safeFetch(url, timeout, c.signal);
4865
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4725
4866
  clearTimeout(t);
4726
- if (!r || !r.ok) return null;
4727
- return await r.text();
4867
+ if (r.url && !validateRedirectUrl(r.url)) return null;
4868
+ return r.ok ? await r.text() : null;
4728
4869
  } catch {
4729
4870
  return null;
4730
4871
  }
@@ -4733,9 +4874,9 @@ async function fetchHeaders(url, timeout = 8e3) {
4733
4874
  try {
4734
4875
  const c = new AbortController();
4735
4876
  const t = setTimeout(() => c.abort(), timeout);
4736
- const r = await safeFetch(url, timeout, c.signal);
4877
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4737
4878
  clearTimeout(t);
4738
- if (!r) return {};
4879
+ if (r.url && !validateRedirectUrl(r.url)) return {};
4739
4880
  const h = {};
4740
4881
  r.headers.forEach((v, k) => {
4741
4882
  h[k.toLowerCase()] = v;
@@ -4750,11 +4891,11 @@ async function fetchWithTiming(url, timeout = 15e3) {
4750
4891
  const c = new AbortController();
4751
4892
  const t = setTimeout(() => c.abort(), timeout);
4752
4893
  const start = Date.now();
4753
- const r = await safeFetch(url, timeout, c.signal);
4754
- clearTimeout(t);
4755
- if (!r || !r.ok) return { html: null, loadTimeMs: null };
4894
+ const r = await fetch(url, { signal: c.signal, headers: { "User-Agent": "Prodlint-WebScanner/1.0" }, redirect: "follow" });
4756
4895
  const html = await r.text();
4757
- return { html, loadTimeMs: Date.now() - start };
4896
+ clearTimeout(t);
4897
+ if (r.url && !validateRedirectUrl(r.url)) return { html: null, loadTimeMs: null };
4898
+ return r.ok ? { html, loadTimeMs: Date.now() - start } : { html: null, loadTimeMs: null };
4758
4899
  } catch {
4759
4900
  return { html: null, loadTimeMs: null };
4760
4901
  }
@@ -4793,6 +4934,10 @@ async function runWebScan(targetUrl) {
4793
4934
  };
4794
4935
  }
4795
4936
  export {
4937
+ reportJson,
4938
+ reportPretty,
4939
+ reportSarif,
4940
+ reportSummary,
4796
4941
  rules,
4797
4942
  runWebScan,
4798
4943
  scan