prodlint 0.9.5 → 0.10.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/LICENSE +21 -21
- package/README.md +334 -329
- package/action.yml +106 -152
- package/dist/cli.js +183 -60
- package/dist/index.js +179 -64
- package/dist/mcp.js +183 -60
- package/package.json +97 -97
package/dist/index.js
CHANGED
|
@@ -516,6 +516,33 @@ async function collectWorkspaceDependencies(root, patterns) {
|
|
|
516
516
|
}
|
|
517
517
|
return deps;
|
|
518
518
|
}
|
|
519
|
+
async function collectNestedPackageDependencies(root) {
|
|
520
|
+
const deps = /* @__PURE__ */ new Set();
|
|
521
|
+
try {
|
|
522
|
+
const pkgFiles = await fg(["**/package.json"], {
|
|
523
|
+
cwd: root,
|
|
524
|
+
absolute: false,
|
|
525
|
+
ignore: ["**/node_modules/**", "package.json"]
|
|
526
|
+
});
|
|
527
|
+
for (const pkgFile of pkgFiles) {
|
|
528
|
+
try {
|
|
529
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
530
|
+
const pkg = JSON.parse(raw);
|
|
531
|
+
if (pkg.name) deps.add(pkg.name);
|
|
532
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
533
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
534
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
535
|
+
deps.add(dep);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
}
|
|
544
|
+
return deps;
|
|
545
|
+
}
|
|
519
546
|
async function buildProjectContext(root, files) {
|
|
520
547
|
let packageJson = null;
|
|
521
548
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -543,6 +570,10 @@ async function buildProjectContext(root, files) {
|
|
|
543
570
|
declaredDependencies.add(dep);
|
|
544
571
|
}
|
|
545
572
|
}
|
|
573
|
+
const nestedDeps = await collectNestedPackageDependencies(root);
|
|
574
|
+
for (const dep of nestedDeps) {
|
|
575
|
+
declaredDependencies.add(dep);
|
|
576
|
+
}
|
|
546
577
|
for (const dep of declaredDependencies) {
|
|
547
578
|
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
548
579
|
if (framework) {
|
|
@@ -4709,6 +4740,14 @@ var STATUS_ICONS = {
|
|
|
4709
4740
|
info: pc.blue
|
|
4710
4741
|
};
|
|
4711
4742
|
|
|
4743
|
+
// src/web-scanner/index.ts
|
|
4744
|
+
import { lookup as dnsLookup } from "dns";
|
|
4745
|
+
import { lookup as dnsLookupAll } from "dns/promises";
|
|
4746
|
+
import { request as httpRequest } from "http";
|
|
4747
|
+
import { request as httpsRequest } from "https";
|
|
4748
|
+
import { BlockList, isIP } from "net";
|
|
4749
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
|
|
4750
|
+
|
|
4712
4751
|
// src/web-scanner/checks.ts
|
|
4713
4752
|
function make(id, name, description, maxPoints, severity, status, details) {
|
|
4714
4753
|
return {
|
|
@@ -4848,76 +4887,153 @@ function getDomain(url) {
|
|
|
4848
4887
|
return url;
|
|
4849
4888
|
}
|
|
4850
4889
|
}
|
|
4851
|
-
var
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4890
|
+
var PRIVATE_RANGES = new BlockList();
|
|
4891
|
+
for (const [network, prefix] of [
|
|
4892
|
+
["0.0.0.0", 8],
|
|
4893
|
+
["10.0.0.0", 8],
|
|
4894
|
+
["100.64.0.0", 10],
|
|
4895
|
+
["127.0.0.0", 8],
|
|
4896
|
+
["169.254.0.0", 16],
|
|
4897
|
+
["172.16.0.0", 12],
|
|
4898
|
+
["192.0.0.0", 24],
|
|
4899
|
+
["192.168.0.0", 16],
|
|
4900
|
+
["198.18.0.0", 15],
|
|
4901
|
+
["224.0.0.0", 3]
|
|
4902
|
+
]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv4");
|
|
4903
|
+
for (const [network, prefix] of [
|
|
4904
|
+
["::", 96],
|
|
4905
|
+
["64:ff9b:1::", 48],
|
|
4906
|
+
["2001::", 32],
|
|
4907
|
+
["fc00::", 7],
|
|
4908
|
+
["fe80::", 10],
|
|
4909
|
+
["fec0::", 10],
|
|
4910
|
+
["ff00::", 8]
|
|
4911
|
+
]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv6");
|
|
4912
|
+
function ipv6Groups(address) {
|
|
4913
|
+
const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
4914
|
+
const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
|
|
4915
|
+
const [head, tail] = hex.split("::");
|
|
4916
|
+
const parse2 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
|
|
4917
|
+
const front = parse2(head);
|
|
4918
|
+
const back = parse2(tail);
|
|
4919
|
+
return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
|
|
4867
4920
|
}
|
|
4868
|
-
function
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4921
|
+
function embeddedIPv4(address) {
|
|
4922
|
+
const g = ipv6Groups(address);
|
|
4923
|
+
const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
|
|
4924
|
+
const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
|
|
4925
|
+
if (zero(0, 5) && g[5] === 65535) return quad(g[6], g[7]);
|
|
4926
|
+
if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return quad(g[6], g[7]);
|
|
4927
|
+
if (g[0] === 8194) return quad(g[1], g[2]);
|
|
4928
|
+
return null;
|
|
4875
4929
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4930
|
+
function isPrivateAddress(address) {
|
|
4931
|
+
const ip = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
|
|
4932
|
+
const family = isIP(ip);
|
|
4933
|
+
if (family === 4) return PRIVATE_RANGES.check(ip, "ipv4");
|
|
4934
|
+
if (family !== 6) return false;
|
|
4935
|
+
const v4 = embeddedIPv4(ip);
|
|
4936
|
+
return v4 !== null && PRIVATE_RANGES.check(v4, "ipv4") || PRIVATE_RANGES.check(ip, "ipv6");
|
|
4937
|
+
}
|
|
4938
|
+
function isPrivateHost(hostname) {
|
|
4939
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
|
|
4940
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal")) return true;
|
|
4941
|
+
return isPrivateAddress(host);
|
|
4942
|
+
}
|
|
4943
|
+
var guardedLookup = (hostname, options, callback) => {
|
|
4944
|
+
dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
4945
|
+
if (err) return callback(err, "");
|
|
4946
|
+
const blocked = addresses.find((a) => isPrivateAddress(a.address));
|
|
4947
|
+
if (blocked) {
|
|
4948
|
+
return callback(Object.assign(new Error(`${hostname} resolves to private address ${blocked.address}`), { code: "EPRIVATEADDR" }), "");
|
|
4949
|
+
}
|
|
4950
|
+
if (options.all) return callback(null, addresses);
|
|
4951
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
4952
|
+
});
|
|
4953
|
+
};
|
|
4954
|
+
var USER_AGENT = "Prodlint-WebScanner/1.0";
|
|
4955
|
+
var MAX_REDIRECTS = 5;
|
|
4956
|
+
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
4957
|
+
function flattenHeaders(res) {
|
|
4958
|
+
const headers = {};
|
|
4959
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
4960
|
+
if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
4886
4961
|
}
|
|
4962
|
+
return headers;
|
|
4887
4963
|
}
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
const
|
|
4896
|
-
|
|
4897
|
-
|
|
4964
|
+
function readBody(res) {
|
|
4965
|
+
const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
|
|
4966
|
+
const decoder = encoding === "gzip" || encoding === "x-gzip" ? createGunzip() : encoding === "deflate" ? createInflate() : encoding === "br" ? createBrotliDecompress() : null;
|
|
4967
|
+
const stream = decoder ? res.pipe(decoder) : res;
|
|
4968
|
+
return new Promise((resolve4, reject) => {
|
|
4969
|
+
const chunks = [];
|
|
4970
|
+
let size = 0;
|
|
4971
|
+
const done = () => resolve4(Buffer.concat(chunks).toString("utf8"));
|
|
4972
|
+
stream.on("data", (chunk) => {
|
|
4973
|
+
if (size >= MAX_BODY_BYTES) return;
|
|
4974
|
+
chunks.push(chunk.subarray(0, MAX_BODY_BYTES - size));
|
|
4975
|
+
size += chunk.length;
|
|
4976
|
+
if (size >= MAX_BODY_BYTES) {
|
|
4977
|
+
done();
|
|
4978
|
+
res.destroy();
|
|
4979
|
+
decoder?.destroy();
|
|
4980
|
+
}
|
|
4898
4981
|
});
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
}
|
|
4982
|
+
stream.on("end", done);
|
|
4983
|
+
stream.on("error", reject);
|
|
4984
|
+
res.on("error", reject);
|
|
4985
|
+
});
|
|
4903
4986
|
}
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4987
|
+
function guardedGet(url, timeoutMs, lookup = guardedLookup) {
|
|
4988
|
+
return new Promise((resolve4) => {
|
|
4989
|
+
let current;
|
|
4990
|
+
const finish = (result) => {
|
|
4991
|
+
clearTimeout(timer);
|
|
4992
|
+
current?.destroy();
|
|
4993
|
+
resolve4(result);
|
|
4994
|
+
};
|
|
4995
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
4996
|
+
const attempt = (target, redirectsLeft) => {
|
|
4997
|
+
if (target.protocol !== "http:" && target.protocol !== "https:" || isPrivateHost(target.hostname)) return finish(null);
|
|
4998
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
4999
|
+
current = send(target, { agent: false, lookup, headers: { "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br" } }, (res) => {
|
|
5000
|
+
const status = res.statusCode ?? 0;
|
|
5001
|
+
const location = res.headers.location;
|
|
5002
|
+
if (status >= 300 && status < 400 && location) {
|
|
5003
|
+
res.resume();
|
|
5004
|
+
let next;
|
|
5005
|
+
try {
|
|
5006
|
+
next = new URL(location, target);
|
|
5007
|
+
} catch {
|
|
5008
|
+
return finish(null);
|
|
5009
|
+
}
|
|
5010
|
+
return redirectsLeft > 0 ? attempt(next, redirectsLeft - 1) : finish(null);
|
|
5011
|
+
}
|
|
5012
|
+
readBody(res).then((body) => finish({ status, headers: flattenHeaders(res), body }), () => finish(null));
|
|
5013
|
+
});
|
|
5014
|
+
current.on("error", () => finish(null));
|
|
5015
|
+
current.end();
|
|
5016
|
+
};
|
|
5017
|
+
try {
|
|
5018
|
+
attempt(new URL(url), MAX_REDIRECTS);
|
|
5019
|
+
} catch {
|
|
5020
|
+
finish(null);
|
|
5021
|
+
}
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
async function fetchText(url, timeout = 8e3) {
|
|
5025
|
+
const r = await guardedGet(url, timeout);
|
|
5026
|
+
return r && r.status >= 200 && r.status < 300 ? r.body : null;
|
|
5027
|
+
}
|
|
5028
|
+
async function fetchPage(url, timeout = 15e3) {
|
|
5029
|
+
const start = Date.now();
|
|
5030
|
+
const r = await guardedGet(url, timeout);
|
|
5031
|
+
const ok = r !== null && r.status >= 200 && r.status < 300;
|
|
5032
|
+
return { headers: r?.headers ?? {}, html: ok ? r.body : null, loadTimeMs: ok ? Date.now() - start : null };
|
|
4917
5033
|
}
|
|
4918
5034
|
async function runWebScan(targetUrl) {
|
|
4919
5035
|
const domain = getDomain(targetUrl);
|
|
4920
|
-
const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory,
|
|
5036
|
+
const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, page] = await Promise.all([
|
|
4921
5037
|
fetchText(`${targetUrl}/robots.txt`),
|
|
4922
5038
|
fetchText(`${targetUrl}/llms.txt`),
|
|
4923
5039
|
fetchText(`${targetUrl}/ai.txt`),
|
|
@@ -4925,10 +5041,9 @@ async function runWebScan(targetUrl) {
|
|
|
4925
5041
|
fetchText(`${targetUrl}/.well-known/agent-card.json`),
|
|
4926
5042
|
fetchText(`${targetUrl}/sitemap.xml`),
|
|
4927
5043
|
fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
|
|
4928
|
-
|
|
4929
|
-
fetchWithTiming(targetUrl)
|
|
5044
|
+
fetchPage(targetUrl)
|
|
4930
5045
|
]);
|
|
4931
|
-
const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory,
|
|
5046
|
+
const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, ...page };
|
|
4932
5047
|
const checks = allChecks.map((fn) => fn(ctx));
|
|
4933
5048
|
const totalPoints = checks.reduce((s, c) => s + c.points, 0);
|
|
4934
5049
|
const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);
|
package/dist/mcp.js
CHANGED
|
@@ -525,6 +525,33 @@ async function collectWorkspaceDependencies(root, patterns) {
|
|
|
525
525
|
}
|
|
526
526
|
return deps;
|
|
527
527
|
}
|
|
528
|
+
async function collectNestedPackageDependencies(root) {
|
|
529
|
+
const deps = /* @__PURE__ */ new Set();
|
|
530
|
+
try {
|
|
531
|
+
const pkgFiles = await fg(["**/package.json"], {
|
|
532
|
+
cwd: root,
|
|
533
|
+
absolute: false,
|
|
534
|
+
ignore: ["**/node_modules/**", "package.json"]
|
|
535
|
+
});
|
|
536
|
+
for (const pkgFile of pkgFiles) {
|
|
537
|
+
try {
|
|
538
|
+
const raw = await readFile(resolve(root, pkgFile), "utf-8");
|
|
539
|
+
const pkg = JSON.parse(raw);
|
|
540
|
+
if (pkg.name) deps.add(pkg.name);
|
|
541
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
542
|
+
if (pkg[key] && typeof pkg[key] === "object") {
|
|
543
|
+
for (const dep of Object.keys(pkg[key])) {
|
|
544
|
+
deps.add(dep);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
return deps;
|
|
554
|
+
}
|
|
528
555
|
async function buildProjectContext(root, files) {
|
|
529
556
|
let packageJson = null;
|
|
530
557
|
let declaredDependencies = /* @__PURE__ */ new Set();
|
|
@@ -552,6 +579,10 @@ async function buildProjectContext(root, files) {
|
|
|
552
579
|
declaredDependencies.add(dep);
|
|
553
580
|
}
|
|
554
581
|
}
|
|
582
|
+
const nestedDeps = await collectNestedPackageDependencies(root);
|
|
583
|
+
for (const dep of nestedDeps) {
|
|
584
|
+
declaredDependencies.add(dep);
|
|
585
|
+
}
|
|
555
586
|
for (const dep of declaredDependencies) {
|
|
556
587
|
const framework = DEPENDENCY_TO_FRAMEWORK[dep];
|
|
557
588
|
if (framework) {
|
|
@@ -4525,6 +4556,14 @@ async function scan(options) {
|
|
|
4525
4556
|
};
|
|
4526
4557
|
}
|
|
4527
4558
|
|
|
4559
|
+
// src/web-scanner/index.ts
|
|
4560
|
+
import { lookup as dnsLookup } from "dns";
|
|
4561
|
+
import { lookup as dnsLookupAll } from "dns/promises";
|
|
4562
|
+
import { request as httpRequest } from "http";
|
|
4563
|
+
import { request as httpsRequest } from "https";
|
|
4564
|
+
import { BlockList, isIP } from "net";
|
|
4565
|
+
import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
|
|
4566
|
+
|
|
4528
4567
|
// src/web-scanner/checks.ts
|
|
4529
4568
|
function make(id, name, description, maxPoints, severity, status, details) {
|
|
4530
4569
|
return {
|
|
@@ -4664,72 +4703,157 @@ function getDomain(url) {
|
|
|
4664
4703
|
return url;
|
|
4665
4704
|
}
|
|
4666
4705
|
}
|
|
4667
|
-
var
|
|
4706
|
+
var PRIVATE_RANGES = new BlockList();
|
|
4707
|
+
for (const [network, prefix] of [
|
|
4708
|
+
["0.0.0.0", 8],
|
|
4709
|
+
["10.0.0.0", 8],
|
|
4710
|
+
["100.64.0.0", 10],
|
|
4711
|
+
["127.0.0.0", 8],
|
|
4712
|
+
["169.254.0.0", 16],
|
|
4713
|
+
["172.16.0.0", 12],
|
|
4714
|
+
["192.0.0.0", 24],
|
|
4715
|
+
["192.168.0.0", 16],
|
|
4716
|
+
["198.18.0.0", 15],
|
|
4717
|
+
["224.0.0.0", 3]
|
|
4718
|
+
]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv4");
|
|
4719
|
+
for (const [network, prefix] of [
|
|
4720
|
+
["::", 96],
|
|
4721
|
+
["64:ff9b:1::", 48],
|
|
4722
|
+
["2001::", 32],
|
|
4723
|
+
["fc00::", 7],
|
|
4724
|
+
["fe80::", 10],
|
|
4725
|
+
["fec0::", 10],
|
|
4726
|
+
["ff00::", 8]
|
|
4727
|
+
]) PRIVATE_RANGES.addSubnet(network, prefix, "ipv6");
|
|
4728
|
+
function ipv6Groups(address) {
|
|
4729
|
+
const quad = address.match(/^(.*:)(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
|
4730
|
+
const hex = quad ? `${quad[1]}${(+quad[2] << 8 | +quad[3]).toString(16)}:${(+quad[4] << 8 | +quad[5]).toString(16)}` : address;
|
|
4731
|
+
const [head, tail] = hex.split("::");
|
|
4732
|
+
const parse2 = (part) => part ? part.split(":").map((g) => parseInt(g, 16)) : [];
|
|
4733
|
+
const front = parse2(head);
|
|
4734
|
+
const back = parse2(tail);
|
|
4735
|
+
return tail === void 0 ? front : [...front, ...Array(8 - front.length - back.length).fill(0), ...back];
|
|
4736
|
+
}
|
|
4737
|
+
function embeddedIPv4(address) {
|
|
4738
|
+
const g = ipv6Groups(address);
|
|
4739
|
+
const quad = (hi, lo) => `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
|
|
4740
|
+
const zero = (from, to) => g.slice(from, to).every((x) => x === 0);
|
|
4741
|
+
if (zero(0, 5) && g[5] === 65535) return quad(g[6], g[7]);
|
|
4742
|
+
if (g[0] === 100 && g[1] === 65435 && zero(2, 6)) return quad(g[6], g[7]);
|
|
4743
|
+
if (g[0] === 8194) return quad(g[1], g[2]);
|
|
4744
|
+
return null;
|
|
4745
|
+
}
|
|
4746
|
+
function isPrivateAddress(address) {
|
|
4747
|
+
const ip = address.replace(/^\[|\]$/g, "").replace(/%.*$/, "");
|
|
4748
|
+
const family = isIP(ip);
|
|
4749
|
+
if (family === 4) return PRIVATE_RANGES.check(ip, "ipv4");
|
|
4750
|
+
if (family !== 6) return false;
|
|
4751
|
+
const v4 = embeddedIPv4(ip);
|
|
4752
|
+
return v4 !== null && PRIVATE_RANGES.check(v4, "ipv4") || PRIVATE_RANGES.check(ip, "ipv6");
|
|
4753
|
+
}
|
|
4668
4754
|
function isPrivateHost(hostname) {
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
4673
|
-
if (ipv4) {
|
|
4674
|
-
const [, a, b] = ipv4.map(Number);
|
|
4675
|
-
if (a === 10 || a === 127 || a === 0) return true;
|
|
4676
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
4677
|
-
if (a === 192 && b === 168) return true;
|
|
4678
|
-
if (a === 169 && b === 254) return true;
|
|
4679
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
4680
|
-
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
4681
|
-
}
|
|
4682
|
-
return false;
|
|
4755
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
|
|
4756
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal")) return true;
|
|
4757
|
+
return isPrivateAddress(host);
|
|
4683
4758
|
}
|
|
4684
|
-
function
|
|
4759
|
+
async function resolvesToPrivateAddress(hostname) {
|
|
4685
4760
|
try {
|
|
4686
|
-
const
|
|
4687
|
-
return
|
|
4761
|
+
const addresses = await dnsLookupAll(hostname.replace(/^\[|\]$/g, ""), { all: true });
|
|
4762
|
+
return addresses.some((a) => isPrivateAddress(a.address));
|
|
4688
4763
|
} catch {
|
|
4689
4764
|
return false;
|
|
4690
4765
|
}
|
|
4691
4766
|
}
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
const
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4767
|
+
var guardedLookup = (hostname, options, callback) => {
|
|
4768
|
+
dnsLookup(hostname, { ...options, all: true }, (err, addresses) => {
|
|
4769
|
+
if (err) return callback(err, "");
|
|
4770
|
+
const blocked = addresses.find((a) => isPrivateAddress(a.address));
|
|
4771
|
+
if (blocked) {
|
|
4772
|
+
return callback(Object.assign(new Error(`${hostname} resolves to private address ${blocked.address}`), { code: "EPRIVATEADDR" }), "");
|
|
4773
|
+
}
|
|
4774
|
+
if (options.all) return callback(null, addresses);
|
|
4775
|
+
callback(null, addresses[0].address, addresses[0].family);
|
|
4776
|
+
});
|
|
4777
|
+
};
|
|
4778
|
+
var USER_AGENT = "Prodlint-WebScanner/1.0";
|
|
4779
|
+
var MAX_REDIRECTS = 5;
|
|
4780
|
+
var MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
4781
|
+
function flattenHeaders(res) {
|
|
4782
|
+
const headers = {};
|
|
4783
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
4784
|
+
if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
4702
4785
|
}
|
|
4786
|
+
return headers;
|
|
4703
4787
|
}
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
const
|
|
4712
|
-
|
|
4713
|
-
|
|
4788
|
+
function readBody(res) {
|
|
4789
|
+
const encoding = String(res.headers["content-encoding"] ?? "").trim().toLowerCase();
|
|
4790
|
+
const decoder = encoding === "gzip" || encoding === "x-gzip" ? createGunzip() : encoding === "deflate" ? createInflate() : encoding === "br" ? createBrotliDecompress() : null;
|
|
4791
|
+
const stream = decoder ? res.pipe(decoder) : res;
|
|
4792
|
+
return new Promise((resolve5, reject) => {
|
|
4793
|
+
const chunks = [];
|
|
4794
|
+
let size = 0;
|
|
4795
|
+
const done = () => resolve5(Buffer.concat(chunks).toString("utf8"));
|
|
4796
|
+
stream.on("data", (chunk) => {
|
|
4797
|
+
if (size >= MAX_BODY_BYTES) return;
|
|
4798
|
+
chunks.push(chunk.subarray(0, MAX_BODY_BYTES - size));
|
|
4799
|
+
size += chunk.length;
|
|
4800
|
+
if (size >= MAX_BODY_BYTES) {
|
|
4801
|
+
done();
|
|
4802
|
+
res.destroy();
|
|
4803
|
+
decoder?.destroy();
|
|
4804
|
+
}
|
|
4714
4805
|
});
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
}
|
|
4806
|
+
stream.on("end", done);
|
|
4807
|
+
stream.on("error", reject);
|
|
4808
|
+
res.on("error", reject);
|
|
4809
|
+
});
|
|
4719
4810
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
const
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4811
|
+
function guardedGet(url, timeoutMs, lookup = guardedLookup) {
|
|
4812
|
+
return new Promise((resolve5) => {
|
|
4813
|
+
let current;
|
|
4814
|
+
const finish = (result) => {
|
|
4815
|
+
clearTimeout(timer);
|
|
4816
|
+
current?.destroy();
|
|
4817
|
+
resolve5(result);
|
|
4818
|
+
};
|
|
4819
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
4820
|
+
const attempt = (target, redirectsLeft) => {
|
|
4821
|
+
if (target.protocol !== "http:" && target.protocol !== "https:" || isPrivateHost(target.hostname)) return finish(null);
|
|
4822
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
4823
|
+
current = send(target, { agent: false, lookup, headers: { "User-Agent": USER_AGENT, "Accept-Encoding": "gzip, deflate, br" } }, (res) => {
|
|
4824
|
+
const status = res.statusCode ?? 0;
|
|
4825
|
+
const location = res.headers.location;
|
|
4826
|
+
if (status >= 300 && status < 400 && location) {
|
|
4827
|
+
res.resume();
|
|
4828
|
+
let next;
|
|
4829
|
+
try {
|
|
4830
|
+
next = new URL(location, target);
|
|
4831
|
+
} catch {
|
|
4832
|
+
return finish(null);
|
|
4833
|
+
}
|
|
4834
|
+
return redirectsLeft > 0 ? attempt(next, redirectsLeft - 1) : finish(null);
|
|
4835
|
+
}
|
|
4836
|
+
readBody(res).then((body) => finish({ status, headers: flattenHeaders(res), body }), () => finish(null));
|
|
4837
|
+
});
|
|
4838
|
+
current.on("error", () => finish(null));
|
|
4839
|
+
current.end();
|
|
4840
|
+
};
|
|
4841
|
+
try {
|
|
4842
|
+
attempt(new URL(url), MAX_REDIRECTS);
|
|
4843
|
+
} catch {
|
|
4844
|
+
finish(null);
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
async function fetchText(url, timeout = 8e3) {
|
|
4849
|
+
const r = await guardedGet(url, timeout);
|
|
4850
|
+
return r && r.status >= 200 && r.status < 300 ? r.body : null;
|
|
4851
|
+
}
|
|
4852
|
+
async function fetchPage(url, timeout = 15e3) {
|
|
4853
|
+
const start = Date.now();
|
|
4854
|
+
const r = await guardedGet(url, timeout);
|
|
4855
|
+
const ok = r !== null && r.status >= 200 && r.status < 300;
|
|
4856
|
+
return { headers: r?.headers ?? {}, html: ok ? r.body : null, loadTimeMs: ok ? Date.now() - start : null };
|
|
4733
4857
|
}
|
|
4734
4858
|
function normalizeUrl(input) {
|
|
4735
4859
|
let url = input.trim();
|
|
@@ -4741,7 +4865,7 @@ function normalizeUrl(input) {
|
|
|
4741
4865
|
}
|
|
4742
4866
|
async function runWebScan(targetUrl) {
|
|
4743
4867
|
const domain = getDomain(targetUrl);
|
|
4744
|
-
const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory,
|
|
4868
|
+
const [robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, page] = await Promise.all([
|
|
4745
4869
|
fetchText(`${targetUrl}/robots.txt`),
|
|
4746
4870
|
fetchText(`${targetUrl}/llms.txt`),
|
|
4747
4871
|
fetchText(`${targetUrl}/ai.txt`),
|
|
@@ -4749,10 +4873,9 @@ async function runWebScan(targetUrl) {
|
|
|
4749
4873
|
fetchText(`${targetUrl}/.well-known/agent-card.json`),
|
|
4750
4874
|
fetchText(`${targetUrl}/sitemap.xml`),
|
|
4751
4875
|
fetchText(`${targetUrl}/.well-known/http-message-signatures-directory`),
|
|
4752
|
-
|
|
4753
|
-
fetchWithTiming(targetUrl)
|
|
4876
|
+
fetchPage(targetUrl)
|
|
4754
4877
|
]);
|
|
4755
|
-
const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory,
|
|
4878
|
+
const ctx = { url: targetUrl, domain, robotsTxt, llmsTxt, aiTxt, tdmRep, agentCard, sitemapXml, httpSigDirectory, ...page };
|
|
4756
4879
|
const checks = allChecks.map((fn) => fn(ctx));
|
|
4757
4880
|
const totalPoints = checks.reduce((s, c) => s + c.points, 0);
|
|
4758
4881
|
const maxPoints = checks.reduce((s, c) => s + c.maxPoints, 0);
|
|
@@ -4854,7 +4977,7 @@ server.tool(
|
|
|
4854
4977
|
};
|
|
4855
4978
|
}
|
|
4856
4979
|
const hostname = new URL(normalizedUrl).hostname;
|
|
4857
|
-
if (isPrivateHost(hostname)) {
|
|
4980
|
+
if (isPrivateHost(hostname) || await resolvesToPrivateAddress(hostname)) {
|
|
4858
4981
|
return {
|
|
4859
4982
|
content: [{ type: "text", text: "Error: Cannot scan private or internal hosts." }],
|
|
4860
4983
|
isError: true
|