@ipation/specbridge 2.4.7 → 2.4.9
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/CHANGELOG.md +39 -1
- package/dist/cli.js +810 -764
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +40 -48
- package/dist/index.js +842 -788
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
package/dist/index.js
CHANGED
|
@@ -1626,6 +1626,308 @@ async function runInference(config, options) {
|
|
|
1626
1626
|
// src/verification/engine.ts
|
|
1627
1627
|
import { Project as Project2 } from "ts-morph";
|
|
1628
1628
|
|
|
1629
|
+
// src/verification/cache.ts
|
|
1630
|
+
import { stat as stat2, readFile as readFile2 } from "fs/promises";
|
|
1631
|
+
import { createHash } from "crypto";
|
|
1632
|
+
var AstCache = class {
|
|
1633
|
+
cache = /* @__PURE__ */ new Map();
|
|
1634
|
+
async get(filePath, project) {
|
|
1635
|
+
try {
|
|
1636
|
+
const stats = await stat2(filePath);
|
|
1637
|
+
const cached = this.cache.get(filePath);
|
|
1638
|
+
if (cached && cached.mtimeMs >= stats.mtimeMs) {
|
|
1639
|
+
return cached.sourceFile;
|
|
1640
|
+
}
|
|
1641
|
+
const content = await readFile2(filePath, "utf-8");
|
|
1642
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
1643
|
+
if (cached && cached.hash === hash) {
|
|
1644
|
+
cached.mtimeMs = stats.mtimeMs;
|
|
1645
|
+
return cached.sourceFile;
|
|
1646
|
+
}
|
|
1647
|
+
let sourceFile = project.getSourceFile(filePath);
|
|
1648
|
+
if (!sourceFile) {
|
|
1649
|
+
sourceFile = project.addSourceFileAtPath(filePath);
|
|
1650
|
+
} else {
|
|
1651
|
+
sourceFile.refreshFromFileSystemSync();
|
|
1652
|
+
}
|
|
1653
|
+
this.cache.set(filePath, {
|
|
1654
|
+
sourceFile,
|
|
1655
|
+
hash,
|
|
1656
|
+
mtimeMs: stats.mtimeMs
|
|
1657
|
+
});
|
|
1658
|
+
return sourceFile;
|
|
1659
|
+
} catch {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
clear() {
|
|
1664
|
+
this.cache.clear();
|
|
1665
|
+
}
|
|
1666
|
+
getStats() {
|
|
1667
|
+
return {
|
|
1668
|
+
entries: this.cache.size,
|
|
1669
|
+
memoryEstimate: this.cache.size * 5e4
|
|
1670
|
+
// Rough estimate: 50KB per AST
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
// src/verification/file-verifier.ts
|
|
1676
|
+
import { createHash as createHash2 } from "crypto";
|
|
1677
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1678
|
+
|
|
1679
|
+
// src/verification/applicability.ts
|
|
1680
|
+
function isConstraintExcepted(filePath, constraint, cwd) {
|
|
1681
|
+
if (!constraint.exceptions) return false;
|
|
1682
|
+
return constraint.exceptions.some((exception) => {
|
|
1683
|
+
if (exception.expiresAt) {
|
|
1684
|
+
const expiryDate = new Date(exception.expiresAt);
|
|
1685
|
+
if (expiryDate < /* @__PURE__ */ new Date()) {
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return matchesPattern(filePath, exception.pattern, { cwd });
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
function shouldApplyConstraintToFile(params) {
|
|
1693
|
+
const { filePath, constraint, cwd, severityFilter } = params;
|
|
1694
|
+
if (!matchesPattern(filePath, constraint.scope, { cwd })) return false;
|
|
1695
|
+
if (severityFilter && !severityFilter.includes(constraint.severity)) return false;
|
|
1696
|
+
if (isConstraintExcepted(filePath, constraint, cwd)) return false;
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/verification/plugins/loader.ts
|
|
1701
|
+
import { existsSync } from "fs";
|
|
1702
|
+
import { join as join3 } from "path";
|
|
1703
|
+
import { pathToFileURL } from "url";
|
|
1704
|
+
import fg2 from "fast-glob";
|
|
1705
|
+
|
|
1706
|
+
// src/utils/logger.ts
|
|
1707
|
+
import pino from "pino";
|
|
1708
|
+
var defaultOptions = {
|
|
1709
|
+
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
1710
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
1711
|
+
base: {
|
|
1712
|
+
service: "specbridge"
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
var destination = pino.destination({
|
|
1716
|
+
fd: 2,
|
|
1717
|
+
// stderr
|
|
1718
|
+
sync: false
|
|
1719
|
+
});
|
|
1720
|
+
var rootLogger = pino(defaultOptions, destination);
|
|
1721
|
+
function getLogger(bindings) {
|
|
1722
|
+
if (!bindings) {
|
|
1723
|
+
return rootLogger;
|
|
1724
|
+
}
|
|
1725
|
+
return rootLogger.child(bindings);
|
|
1726
|
+
}
|
|
1727
|
+
var logger = getLogger();
|
|
1728
|
+
|
|
1729
|
+
// src/verification/plugins/loader.ts
|
|
1730
|
+
var PluginLoader = class {
|
|
1731
|
+
plugins = /* @__PURE__ */ new Map();
|
|
1732
|
+
loaded = false;
|
|
1733
|
+
loadErrors = [];
|
|
1734
|
+
logger = getLogger({ module: "verification.plugins.loader" });
|
|
1735
|
+
/**
|
|
1736
|
+
* Load all plugins from the specified base path
|
|
1737
|
+
*
|
|
1738
|
+
* @param basePath - Project root directory (usually cwd)
|
|
1739
|
+
*/
|
|
1740
|
+
async loadPlugins(basePath) {
|
|
1741
|
+
const verifiersDir = join3(basePath, ".specbridge", "verifiers");
|
|
1742
|
+
if (!existsSync(verifiersDir)) {
|
|
1743
|
+
this.loaded = true;
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const files = await fg2("**/*.{ts,js}", {
|
|
1747
|
+
cwd: verifiersDir,
|
|
1748
|
+
absolute: true,
|
|
1749
|
+
ignore: ["**/*.test.{ts,js}", "**/*.d.ts"]
|
|
1750
|
+
});
|
|
1751
|
+
for (const file of files) {
|
|
1752
|
+
try {
|
|
1753
|
+
await this.loadPlugin(file);
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1756
|
+
this.loadErrors.push({ file, error: message });
|
|
1757
|
+
this.logger.warn({ file, error: message }, "Failed to load plugin");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
this.loaded = true;
|
|
1761
|
+
if (this.plugins.size > 0) {
|
|
1762
|
+
this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
|
|
1763
|
+
}
|
|
1764
|
+
if (this.loadErrors.length > 0) {
|
|
1765
|
+
this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Load a single plugin file
|
|
1770
|
+
*/
|
|
1771
|
+
async loadPlugin(filePath) {
|
|
1772
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
1773
|
+
const module = await import(fileUrl);
|
|
1774
|
+
const plugin = module.default || module.plugin;
|
|
1775
|
+
if (!plugin) {
|
|
1776
|
+
throw new Error('Plugin must export a default or named "plugin" export');
|
|
1777
|
+
}
|
|
1778
|
+
this.validatePlugin(plugin, filePath);
|
|
1779
|
+
if (this.plugins.has(plugin.metadata.id)) {
|
|
1780
|
+
throw new Error(
|
|
1781
|
+
`Plugin ID "${plugin.metadata.id}" is already registered. Each plugin must have a unique ID.`
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
this.plugins.set(plugin.metadata.id, plugin);
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Validate plugin structure and metadata
|
|
1788
|
+
*/
|
|
1789
|
+
validatePlugin(plugin, _filePath) {
|
|
1790
|
+
if (!plugin.metadata) {
|
|
1791
|
+
throw new Error('Plugin must have a "metadata" property');
|
|
1792
|
+
}
|
|
1793
|
+
if (!plugin.metadata.id || typeof plugin.metadata.id !== "string") {
|
|
1794
|
+
throw new Error('Plugin metadata must have a string "id" property');
|
|
1795
|
+
}
|
|
1796
|
+
if (!plugin.metadata.version || typeof plugin.metadata.version !== "string") {
|
|
1797
|
+
throw new Error('Plugin metadata must have a string "version" property');
|
|
1798
|
+
}
|
|
1799
|
+
const idPattern = /^[a-z][a-z0-9-]*$/;
|
|
1800
|
+
if (!idPattern.test(plugin.metadata.id)) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Plugin ID "${plugin.metadata.id}" is invalid. ID must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`
|
|
1803
|
+
);
|
|
1804
|
+
}
|
|
1805
|
+
if (typeof plugin.createVerifier !== "function") {
|
|
1806
|
+
throw new Error('Plugin must have a "createVerifier" function');
|
|
1807
|
+
}
|
|
1808
|
+
let verifier;
|
|
1809
|
+
try {
|
|
1810
|
+
verifier = plugin.createVerifier();
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1813
|
+
throw new Error(`createVerifier() threw an error: ${message}`);
|
|
1814
|
+
}
|
|
1815
|
+
if (!verifier || typeof verifier !== "object") {
|
|
1816
|
+
throw new Error("createVerifier() must return an object");
|
|
1817
|
+
}
|
|
1818
|
+
if (!verifier.id || typeof verifier.id !== "string") {
|
|
1819
|
+
throw new Error('Verifier must have a string "id" property');
|
|
1820
|
+
}
|
|
1821
|
+
if (!verifier.name || typeof verifier.name !== "string") {
|
|
1822
|
+
throw new Error('Verifier must have a string "name" property');
|
|
1823
|
+
}
|
|
1824
|
+
if (!verifier.description || typeof verifier.description !== "string") {
|
|
1825
|
+
throw new Error('Verifier must have a string "description" property');
|
|
1826
|
+
}
|
|
1827
|
+
if (typeof verifier.verify !== "function") {
|
|
1828
|
+
throw new Error('Verifier must have a "verify" method');
|
|
1829
|
+
}
|
|
1830
|
+
if (verifier.id !== plugin.metadata.id) {
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`Verifier ID "${verifier.id}" does not match plugin metadata ID "${plugin.metadata.id}". These must be identical.`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Get a verifier instance by ID
|
|
1838
|
+
*
|
|
1839
|
+
* @param id - Verifier ID
|
|
1840
|
+
* @returns Verifier instance or null if not found
|
|
1841
|
+
*/
|
|
1842
|
+
getVerifier(id) {
|
|
1843
|
+
const plugin = this.plugins.get(id);
|
|
1844
|
+
return plugin ? plugin.createVerifier() : null;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Get a plugin by ID (includes paramsSchema)
|
|
1848
|
+
*
|
|
1849
|
+
* @param id - Plugin ID
|
|
1850
|
+
* @returns Plugin or null if not found
|
|
1851
|
+
*/
|
|
1852
|
+
getPlugin(id) {
|
|
1853
|
+
return this.plugins.get(id) || null;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Validate params against a plugin's paramsSchema
|
|
1857
|
+
*
|
|
1858
|
+
* @param id - Plugin ID
|
|
1859
|
+
* @param params - Parameters to validate
|
|
1860
|
+
* @returns Validation result with success flag and error message if failed
|
|
1861
|
+
*/
|
|
1862
|
+
validateParams(id, params) {
|
|
1863
|
+
const plugin = this.plugins.get(id);
|
|
1864
|
+
if (!plugin) {
|
|
1865
|
+
return { success: false, error: `Plugin ${id} not found` };
|
|
1866
|
+
}
|
|
1867
|
+
if (!plugin.paramsSchema) {
|
|
1868
|
+
return { success: true };
|
|
1869
|
+
}
|
|
1870
|
+
if (!params) {
|
|
1871
|
+
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
|
|
1874
|
+
return {
|
|
1875
|
+
success: false,
|
|
1876
|
+
error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
const schema = plugin.paramsSchema;
|
|
1880
|
+
if (schema.safeParse) {
|
|
1881
|
+
const result = schema.safeParse(params);
|
|
1882
|
+
if (!result.success) {
|
|
1883
|
+
const errors = result.error?.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ") || "Validation failed";
|
|
1884
|
+
return { success: false, error: `Invalid params for ${id}: ${errors}` };
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
try {
|
|
1888
|
+
schema.parse(params);
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1891
|
+
return { success: false, error: `Invalid params for ${id}: ${message}` };
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
return { success: true };
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Get all registered plugin IDs
|
|
1898
|
+
*/
|
|
1899
|
+
getPluginIds() {
|
|
1900
|
+
return Array.from(this.plugins.keys());
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Check if plugins have been loaded
|
|
1904
|
+
*/
|
|
1905
|
+
isLoaded() {
|
|
1906
|
+
return this.loaded;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Get load errors (for diagnostics)
|
|
1910
|
+
*/
|
|
1911
|
+
getLoadErrors() {
|
|
1912
|
+
return [...this.loadErrors];
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Reset the loader (useful for testing)
|
|
1916
|
+
*/
|
|
1917
|
+
reset() {
|
|
1918
|
+
this.plugins.clear();
|
|
1919
|
+
this.loaded = false;
|
|
1920
|
+
this.loadErrors = [];
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
var pluginLoader = null;
|
|
1924
|
+
function getPluginLoader() {
|
|
1925
|
+
if (!pluginLoader) {
|
|
1926
|
+
pluginLoader = new PluginLoader();
|
|
1927
|
+
}
|
|
1928
|
+
return pluginLoader;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1629
1931
|
// src/verification/verifiers/base.ts
|
|
1630
1932
|
function defineVerifierPlugin(plugin) {
|
|
1631
1933
|
return plugin;
|
|
@@ -2464,467 +2766,236 @@ var ComplexityVerifier = class {
|
|
|
2464
2766
|
createViolation({
|
|
2465
2767
|
decisionId,
|
|
2466
2768
|
constraintId: constraint.id,
|
|
2467
|
-
type: constraint.type,
|
|
2468
|
-
severity: constraint.severity,
|
|
2469
|
-
message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
|
|
2470
|
-
file: filePath,
|
|
2471
|
-
line: fn.getStartLineNumber(),
|
|
2472
|
-
suggestion: "Reduce nesting by using early returns or extracting functions"
|
|
2473
|
-
})
|
|
2474
|
-
);
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
return violations;
|
|
2479
|
-
}
|
|
2480
|
-
};
|
|
2481
|
-
|
|
2482
|
-
// src/verification/verifiers/security.ts
|
|
2483
|
-
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
2484
|
-
var SECRET_NAME_RE = /(api[_-]?key|password|secret|token)/i;
|
|
2485
|
-
function isStringLiteralLike(node) {
|
|
2486
|
-
const k = node.getKind();
|
|
2487
|
-
return k === SyntaxKind3.StringLiteral || k === SyntaxKind3.NoSubstitutionTemplateLiteral;
|
|
2488
|
-
}
|
|
2489
|
-
var SecurityVerifier = class {
|
|
2490
|
-
id = "security";
|
|
2491
|
-
name = "Security Verifier";
|
|
2492
|
-
description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
|
|
2493
|
-
async verify(ctx) {
|
|
2494
|
-
const violations = [];
|
|
2495
|
-
const { sourceFile, constraint, decisionId, filePath } = ctx;
|
|
2496
|
-
const rule = constraint.rule.toLowerCase();
|
|
2497
|
-
const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
|
|
2498
|
-
const checkEval = rule.includes("eval") || rule.includes("function constructor");
|
|
2499
|
-
const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
|
|
2500
|
-
const checkSql = rule.includes("sql") || rule.includes("injection");
|
|
2501
|
-
const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
|
|
2502
|
-
if (checkSecrets) {
|
|
2503
|
-
for (const vd of sourceFile.getVariableDeclarations()) {
|
|
2504
|
-
const name = vd.getName();
|
|
2505
|
-
if (!SECRET_NAME_RE.test(name)) continue;
|
|
2506
|
-
const init = vd.getInitializer();
|
|
2507
|
-
if (!init || !isStringLiteralLike(init)) continue;
|
|
2508
|
-
const value = init.getText().slice(1, -1);
|
|
2509
|
-
if (value.length === 0) continue;
|
|
2510
|
-
violations.push(
|
|
2511
|
-
createViolation({
|
|
2512
|
-
decisionId,
|
|
2513
|
-
constraintId: constraint.id,
|
|
2514
|
-
type: constraint.type,
|
|
2515
|
-
severity: constraint.severity,
|
|
2516
|
-
message: `Possible hardcoded secret in variable "${name}"`,
|
|
2517
|
-
file: filePath,
|
|
2518
|
-
line: vd.getStartLineNumber(),
|
|
2519
|
-
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2520
|
-
})
|
|
2521
|
-
);
|
|
2522
|
-
}
|
|
2523
|
-
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2524
|
-
const propName = pa.getNameNode().getText();
|
|
2525
|
-
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2526
|
-
const init = pa.getInitializer();
|
|
2527
|
-
if (!init || !isStringLiteralLike(init)) continue;
|
|
2528
|
-
violations.push(
|
|
2529
|
-
createViolation({
|
|
2530
|
-
decisionId,
|
|
2531
|
-
constraintId: constraint.id,
|
|
2532
|
-
type: constraint.type,
|
|
2533
|
-
severity: constraint.severity,
|
|
2534
|
-
message: `Possible hardcoded secret in object property ${propName}`,
|
|
2535
|
-
file: filePath,
|
|
2536
|
-
line: pa.getStartLineNumber(),
|
|
2537
|
-
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2538
|
-
})
|
|
2539
|
-
);
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
if (checkEval) {
|
|
2543
|
-
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2544
|
-
const exprText = call.getExpression().getText();
|
|
2545
|
-
if (exprText === "eval" || exprText === "Function") {
|
|
2546
|
-
violations.push(
|
|
2547
|
-
createViolation({
|
|
2548
|
-
decisionId,
|
|
2549
|
-
constraintId: constraint.id,
|
|
2550
|
-
type: constraint.type,
|
|
2551
|
-
severity: constraint.severity,
|
|
2552
|
-
message: `Unsafe dynamic code execution via ${exprText}()`,
|
|
2553
|
-
file: filePath,
|
|
2554
|
-
line: call.getStartLineNumber(),
|
|
2555
|
-
suggestion: "Avoid eval/Function; use safer alternatives"
|
|
2556
|
-
})
|
|
2557
|
-
);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
if (checkXss) {
|
|
2562
|
-
for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2563
|
-
const left = bin.getLeft();
|
|
2564
|
-
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2565
|
-
if (!propertyAccess) continue;
|
|
2566
|
-
if (propertyAccess.getName() === "innerHTML") {
|
|
2567
|
-
violations.push(
|
|
2568
|
-
createViolation({
|
|
2569
|
-
decisionId,
|
|
2570
|
-
constraintId: constraint.id,
|
|
2571
|
-
type: constraint.type,
|
|
2572
|
-
severity: constraint.severity,
|
|
2573
|
-
message: "Potential XSS: assignment to innerHTML",
|
|
2574
|
-
file: filePath,
|
|
2575
|
-
line: bin.getStartLineNumber(),
|
|
2576
|
-
suggestion: "Prefer textContent or a safe templating/escaping strategy"
|
|
2577
|
-
})
|
|
2578
|
-
);
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2582
|
-
violations.push(
|
|
2583
|
-
createViolation({
|
|
2584
|
-
decisionId,
|
|
2585
|
-
constraintId: constraint.id,
|
|
2586
|
-
type: constraint.type,
|
|
2587
|
-
severity: constraint.severity,
|
|
2588
|
-
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2589
|
-
file: filePath,
|
|
2590
|
-
line: 1,
|
|
2591
|
-
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2592
|
-
})
|
|
2593
|
-
);
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
if (checkSql) {
|
|
2597
|
-
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2598
|
-
const expr = call.getExpression();
|
|
2599
|
-
const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2600
|
-
if (!propertyAccess) continue;
|
|
2601
|
-
const name = propertyAccess.getName();
|
|
2602
|
-
if (name !== "query" && name !== "execute") continue;
|
|
2603
|
-
const arg = call.getArguments()[0];
|
|
2604
|
-
if (!arg) continue;
|
|
2605
|
-
const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
|
|
2606
|
-
const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
|
|
2607
|
-
if (!isTemplate && !isConcat) continue;
|
|
2608
|
-
const text = arg.getText().toLowerCase();
|
|
2609
|
-
if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
|
|
2610
|
-
continue;
|
|
2611
|
-
}
|
|
2612
|
-
violations.push(
|
|
2613
|
-
createViolation({
|
|
2614
|
-
decisionId,
|
|
2615
|
-
constraintId: constraint.id,
|
|
2616
|
-
type: constraint.type,
|
|
2617
|
-
severity: constraint.severity,
|
|
2618
|
-
message: "Potential SQL injection: dynamically constructed SQL query",
|
|
2619
|
-
file: filePath,
|
|
2620
|
-
line: call.getStartLineNumber(),
|
|
2621
|
-
suggestion: "Use parameterized queries / prepared statements"
|
|
2622
|
-
})
|
|
2623
|
-
);
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
if (checkProto) {
|
|
2627
|
-
const text = sourceFile.getFullText();
|
|
2628
|
-
if (text.includes("__proto__") || text.includes("constructor.prototype")) {
|
|
2629
|
-
violations.push(
|
|
2630
|
-
createViolation({
|
|
2631
|
-
decisionId,
|
|
2632
|
-
constraintId: constraint.id,
|
|
2633
|
-
type: constraint.type,
|
|
2634
|
-
severity: constraint.severity,
|
|
2635
|
-
message: "Potential prototype pollution pattern detected",
|
|
2636
|
-
file: filePath,
|
|
2637
|
-
line: 1,
|
|
2638
|
-
suggestion: "Avoid writing to __proto__/prototype; validate object keys"
|
|
2639
|
-
})
|
|
2640
|
-
);
|
|
2769
|
+
type: constraint.type,
|
|
2770
|
+
severity: constraint.severity,
|
|
2771
|
+
message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
|
|
2772
|
+
file: filePath,
|
|
2773
|
+
line: fn.getStartLineNumber(),
|
|
2774
|
+
suggestion: "Reduce nesting by using early returns or extracting functions"
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2641
2778
|
}
|
|
2642
2779
|
}
|
|
2643
2780
|
return violations;
|
|
2644
2781
|
}
|
|
2645
2782
|
};
|
|
2646
2783
|
|
|
2647
|
-
// src/verification/verifiers/
|
|
2648
|
-
import { SyntaxKind as
|
|
2649
|
-
var
|
|
2650
|
-
function
|
|
2651
|
-
const
|
|
2652
|
-
|
|
2653
|
-
if (part.startsWith(":")) continue;
|
|
2654
|
-
if (!/^[a-z0-9-]+$/.test(part)) return false;
|
|
2655
|
-
}
|
|
2656
|
-
return true;
|
|
2784
|
+
// src/verification/verifiers/security.ts
|
|
2785
|
+
import { SyntaxKind as SyntaxKind3 } from "ts-morph";
|
|
2786
|
+
var SECRET_NAME_RE = /(api[_-]?key|password|secret|token)/i;
|
|
2787
|
+
function isStringLiteralLike(node) {
|
|
2788
|
+
const k = node.getKind();
|
|
2789
|
+
return k === SyntaxKind3.StringLiteral || k === SyntaxKind3.NoSubstitutionTemplateLiteral;
|
|
2657
2790
|
}
|
|
2658
|
-
var
|
|
2659
|
-
id = "
|
|
2660
|
-
name = "
|
|
2661
|
-
description = "
|
|
2791
|
+
var SecurityVerifier = class {
|
|
2792
|
+
id = "security";
|
|
2793
|
+
name = "Security Verifier";
|
|
2794
|
+
description = "Detects common security footguns (secrets, eval, XSS/SQL injection heuristics)";
|
|
2662
2795
|
async verify(ctx) {
|
|
2663
2796
|
const violations = [];
|
|
2664
2797
|
const { sourceFile, constraint, decisionId, filePath } = ctx;
|
|
2665
2798
|
const rule = constraint.rule.toLowerCase();
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
const
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
if (!isKebabPath(pathValue)) {
|
|
2799
|
+
const checkSecrets = rule.includes("secret") || rule.includes("password") || rule.includes("token") || rule.includes("api key") || rule.includes("hardcoded");
|
|
2800
|
+
const checkEval = rule.includes("eval") || rule.includes("function constructor");
|
|
2801
|
+
const checkXss = rule.includes("xss") || rule.includes("innerhtml") || rule.includes("dangerouslysetinnerhtml");
|
|
2802
|
+
const checkSql = rule.includes("sql") || rule.includes("injection");
|
|
2803
|
+
const checkProto = rule.includes("prototype pollution") || rule.includes("__proto__");
|
|
2804
|
+
if (checkSecrets) {
|
|
2805
|
+
for (const vd of sourceFile.getVariableDeclarations()) {
|
|
2806
|
+
const name = vd.getName();
|
|
2807
|
+
if (!SECRET_NAME_RE.test(name)) continue;
|
|
2808
|
+
const init = vd.getInitializer();
|
|
2809
|
+
if (!init || !isStringLiteralLike(init)) continue;
|
|
2810
|
+
const value = init.getText().slice(1, -1);
|
|
2811
|
+
if (value.length === 0) continue;
|
|
2680
2812
|
violations.push(
|
|
2681
2813
|
createViolation({
|
|
2682
2814
|
decisionId,
|
|
2683
2815
|
constraintId: constraint.id,
|
|
2684
2816
|
type: constraint.type,
|
|
2685
2817
|
severity: constraint.severity,
|
|
2686
|
-
message: `
|
|
2818
|
+
message: `Possible hardcoded secret in variable "${name}"`,
|
|
2687
2819
|
file: filePath,
|
|
2688
|
-
line:
|
|
2689
|
-
suggestion: "
|
|
2820
|
+
line: vd.getStartLineNumber(),
|
|
2821
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2822
|
+
})
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2826
|
+
const propName = pa.getNameNode().getText();
|
|
2827
|
+
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2828
|
+
const init = pa.getInitializer();
|
|
2829
|
+
if (!init || !isStringLiteralLike(init)) continue;
|
|
2830
|
+
violations.push(
|
|
2831
|
+
createViolation({
|
|
2832
|
+
decisionId,
|
|
2833
|
+
constraintId: constraint.id,
|
|
2834
|
+
type: constraint.type,
|
|
2835
|
+
severity: constraint.severity,
|
|
2836
|
+
message: `Possible hardcoded secret in object property ${propName}`,
|
|
2837
|
+
file: filePath,
|
|
2838
|
+
line: pa.getStartLineNumber(),
|
|
2839
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2690
2840
|
})
|
|
2691
2841
|
);
|
|
2692
2842
|
}
|
|
2693
2843
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
};
|
|
2713
|
-
var destination = pino.destination({
|
|
2714
|
-
fd: 2,
|
|
2715
|
-
// stderr
|
|
2716
|
-
sync: false
|
|
2717
|
-
});
|
|
2718
|
-
var rootLogger = pino(defaultOptions, destination);
|
|
2719
|
-
function getLogger(bindings) {
|
|
2720
|
-
if (!bindings) {
|
|
2721
|
-
return rootLogger;
|
|
2722
|
-
}
|
|
2723
|
-
return rootLogger.child(bindings);
|
|
2724
|
-
}
|
|
2725
|
-
var logger = getLogger();
|
|
2726
|
-
|
|
2727
|
-
// src/verification/plugins/loader.ts
|
|
2728
|
-
var PluginLoader = class {
|
|
2729
|
-
plugins = /* @__PURE__ */ new Map();
|
|
2730
|
-
loaded = false;
|
|
2731
|
-
loadErrors = [];
|
|
2732
|
-
logger = getLogger({ module: "verification.plugins.loader" });
|
|
2733
|
-
/**
|
|
2734
|
-
* Load all plugins from the specified base path
|
|
2735
|
-
*
|
|
2736
|
-
* @param basePath - Project root directory (usually cwd)
|
|
2737
|
-
*/
|
|
2738
|
-
async loadPlugins(basePath) {
|
|
2739
|
-
const verifiersDir = join3(basePath, ".specbridge", "verifiers");
|
|
2740
|
-
if (!existsSync(verifiersDir)) {
|
|
2741
|
-
this.loaded = true;
|
|
2742
|
-
return;
|
|
2743
|
-
}
|
|
2744
|
-
const files = await fg2("**/*.{ts,js}", {
|
|
2745
|
-
cwd: verifiersDir,
|
|
2746
|
-
absolute: true,
|
|
2747
|
-
ignore: ["**/*.test.{ts,js}", "**/*.d.ts"]
|
|
2748
|
-
});
|
|
2749
|
-
for (const file of files) {
|
|
2750
|
-
try {
|
|
2751
|
-
await this.loadPlugin(file);
|
|
2752
|
-
} catch (error) {
|
|
2753
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2754
|
-
this.loadErrors.push({ file, error: message });
|
|
2755
|
-
this.logger.warn({ file, error: message }, "Failed to load plugin");
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
this.loaded = true;
|
|
2759
|
-
if (this.plugins.size > 0) {
|
|
2760
|
-
this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
|
|
2761
|
-
}
|
|
2762
|
-
if (this.loadErrors.length > 0) {
|
|
2763
|
-
this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
/**
|
|
2767
|
-
* Load a single plugin file
|
|
2768
|
-
*/
|
|
2769
|
-
async loadPlugin(filePath) {
|
|
2770
|
-
const fileUrl = pathToFileURL(filePath).href;
|
|
2771
|
-
const module = await import(fileUrl);
|
|
2772
|
-
const plugin = module.default || module.plugin;
|
|
2773
|
-
if (!plugin) {
|
|
2774
|
-
throw new Error('Plugin must export a default or named "plugin" export');
|
|
2775
|
-
}
|
|
2776
|
-
this.validatePlugin(plugin, filePath);
|
|
2777
|
-
if (this.plugins.has(plugin.metadata.id)) {
|
|
2778
|
-
throw new Error(
|
|
2779
|
-
`Plugin ID "${plugin.metadata.id}" is already registered. Each plugin must have a unique ID.`
|
|
2780
|
-
);
|
|
2781
|
-
}
|
|
2782
|
-
this.plugins.set(plugin.metadata.id, plugin);
|
|
2783
|
-
}
|
|
2784
|
-
/**
|
|
2785
|
-
* Validate plugin structure and metadata
|
|
2786
|
-
*/
|
|
2787
|
-
validatePlugin(plugin, _filePath) {
|
|
2788
|
-
if (!plugin.metadata) {
|
|
2789
|
-
throw new Error('Plugin must have a "metadata" property');
|
|
2790
|
-
}
|
|
2791
|
-
if (!plugin.metadata.id || typeof plugin.metadata.id !== "string") {
|
|
2792
|
-
throw new Error('Plugin metadata must have a string "id" property');
|
|
2793
|
-
}
|
|
2794
|
-
if (!plugin.metadata.version || typeof plugin.metadata.version !== "string") {
|
|
2795
|
-
throw new Error('Plugin metadata must have a string "version" property');
|
|
2796
|
-
}
|
|
2797
|
-
const idPattern = /^[a-z][a-z0-9-]*$/;
|
|
2798
|
-
if (!idPattern.test(plugin.metadata.id)) {
|
|
2799
|
-
throw new Error(
|
|
2800
|
-
`Plugin ID "${plugin.metadata.id}" is invalid. ID must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.`
|
|
2801
|
-
);
|
|
2802
|
-
}
|
|
2803
|
-
if (typeof plugin.createVerifier !== "function") {
|
|
2804
|
-
throw new Error('Plugin must have a "createVerifier" function');
|
|
2805
|
-
}
|
|
2806
|
-
let verifier;
|
|
2807
|
-
try {
|
|
2808
|
-
verifier = plugin.createVerifier();
|
|
2809
|
-
} catch (error) {
|
|
2810
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2811
|
-
throw new Error(`createVerifier() threw an error: ${message}`);
|
|
2812
|
-
}
|
|
2813
|
-
if (!verifier || typeof verifier !== "object") {
|
|
2814
|
-
throw new Error("createVerifier() must return an object");
|
|
2815
|
-
}
|
|
2816
|
-
if (!verifier.id || typeof verifier.id !== "string") {
|
|
2817
|
-
throw new Error('Verifier must have a string "id" property');
|
|
2818
|
-
}
|
|
2819
|
-
if (!verifier.name || typeof verifier.name !== "string") {
|
|
2820
|
-
throw new Error('Verifier must have a string "name" property');
|
|
2821
|
-
}
|
|
2822
|
-
if (!verifier.description || typeof verifier.description !== "string") {
|
|
2823
|
-
throw new Error('Verifier must have a string "description" property');
|
|
2824
|
-
}
|
|
2825
|
-
if (typeof verifier.verify !== "function") {
|
|
2826
|
-
throw new Error('Verifier must have a "verify" method');
|
|
2827
|
-
}
|
|
2828
|
-
if (verifier.id !== plugin.metadata.id) {
|
|
2829
|
-
throw new Error(
|
|
2830
|
-
`Verifier ID "${verifier.id}" does not match plugin metadata ID "${plugin.metadata.id}". These must be identical.`
|
|
2831
|
-
);
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
/**
|
|
2835
|
-
* Get a verifier instance by ID
|
|
2836
|
-
*
|
|
2837
|
-
* @param id - Verifier ID
|
|
2838
|
-
* @returns Verifier instance or null if not found
|
|
2839
|
-
*/
|
|
2840
|
-
getVerifier(id) {
|
|
2841
|
-
const plugin = this.plugins.get(id);
|
|
2842
|
-
return plugin ? plugin.createVerifier() : null;
|
|
2843
|
-
}
|
|
2844
|
-
/**
|
|
2845
|
-
* Get a plugin by ID (includes paramsSchema)
|
|
2846
|
-
*
|
|
2847
|
-
* @param id - Plugin ID
|
|
2848
|
-
* @returns Plugin or null if not found
|
|
2849
|
-
*/
|
|
2850
|
-
getPlugin(id) {
|
|
2851
|
-
return this.plugins.get(id) || null;
|
|
2852
|
-
}
|
|
2853
|
-
/**
|
|
2854
|
-
* Validate params against a plugin's paramsSchema
|
|
2855
|
-
*
|
|
2856
|
-
* @param id - Plugin ID
|
|
2857
|
-
* @param params - Parameters to validate
|
|
2858
|
-
* @returns Validation result with success flag and error message if failed
|
|
2859
|
-
*/
|
|
2860
|
-
validateParams(id, params) {
|
|
2861
|
-
const plugin = this.plugins.get(id);
|
|
2862
|
-
if (!plugin) {
|
|
2863
|
-
return { success: false, error: `Plugin ${id} not found` };
|
|
2864
|
-
}
|
|
2865
|
-
if (!plugin.paramsSchema) {
|
|
2866
|
-
return { success: true };
|
|
2867
|
-
}
|
|
2868
|
-
if (!params) {
|
|
2869
|
-
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
2844
|
+
if (checkEval) {
|
|
2845
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2846
|
+
const exprText = call.getExpression().getText();
|
|
2847
|
+
if (exprText === "eval" || exprText === "Function") {
|
|
2848
|
+
violations.push(
|
|
2849
|
+
createViolation({
|
|
2850
|
+
decisionId,
|
|
2851
|
+
constraintId: constraint.id,
|
|
2852
|
+
type: constraint.type,
|
|
2853
|
+
severity: constraint.severity,
|
|
2854
|
+
message: `Unsafe dynamic code execution via ${exprText}()`,
|
|
2855
|
+
file: filePath,
|
|
2856
|
+
line: call.getStartLineNumber(),
|
|
2857
|
+
suggestion: "Avoid eval/Function; use safer alternatives"
|
|
2858
|
+
})
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2870
2862
|
}
|
|
2871
|
-
if (
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2863
|
+
if (checkXss) {
|
|
2864
|
+
for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2865
|
+
const left = bin.getLeft();
|
|
2866
|
+
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2867
|
+
if (!propertyAccess) continue;
|
|
2868
|
+
if (propertyAccess.getName() === "innerHTML") {
|
|
2869
|
+
violations.push(
|
|
2870
|
+
createViolation({
|
|
2871
|
+
decisionId,
|
|
2872
|
+
constraintId: constraint.id,
|
|
2873
|
+
type: constraint.type,
|
|
2874
|
+
severity: constraint.severity,
|
|
2875
|
+
message: "Potential XSS: assignment to innerHTML",
|
|
2876
|
+
file: filePath,
|
|
2877
|
+
line: bin.getStartLineNumber(),
|
|
2878
|
+
suggestion: "Prefer textContent or a safe templating/escaping strategy"
|
|
2879
|
+
})
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2884
|
+
violations.push(
|
|
2885
|
+
createViolation({
|
|
2886
|
+
decisionId,
|
|
2887
|
+
constraintId: constraint.id,
|
|
2888
|
+
type: constraint.type,
|
|
2889
|
+
severity: constraint.severity,
|
|
2890
|
+
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2891
|
+
file: filePath,
|
|
2892
|
+
line: 1,
|
|
2893
|
+
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2894
|
+
})
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2876
2897
|
}
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2898
|
+
if (checkSql) {
|
|
2899
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2900
|
+
const expr = call.getExpression();
|
|
2901
|
+
const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2902
|
+
if (!propertyAccess) continue;
|
|
2903
|
+
const name = propertyAccess.getName();
|
|
2904
|
+
if (name !== "query" && name !== "execute") continue;
|
|
2905
|
+
const arg = call.getArguments()[0];
|
|
2906
|
+
if (!arg) continue;
|
|
2907
|
+
const isTemplate = arg.getKind() === SyntaxKind3.TemplateExpression;
|
|
2908
|
+
const isConcat = arg.getKind() === SyntaxKind3.BinaryExpression && arg.getText().includes("+");
|
|
2909
|
+
if (!isTemplate && !isConcat) continue;
|
|
2910
|
+
const text = arg.getText().toLowerCase();
|
|
2911
|
+
if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
violations.push(
|
|
2915
|
+
createViolation({
|
|
2916
|
+
decisionId,
|
|
2917
|
+
constraintId: constraint.id,
|
|
2918
|
+
type: constraint.type,
|
|
2919
|
+
severity: constraint.severity,
|
|
2920
|
+
message: "Potential SQL injection: dynamically constructed SQL query",
|
|
2921
|
+
file: filePath,
|
|
2922
|
+
line: call.getStartLineNumber(),
|
|
2923
|
+
suggestion: "Use parameterized queries / prepared statements"
|
|
2924
|
+
})
|
|
2925
|
+
);
|
|
2883
2926
|
}
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2927
|
+
}
|
|
2928
|
+
if (checkProto) {
|
|
2929
|
+
const text = sourceFile.getFullText();
|
|
2930
|
+
if (text.includes("__proto__") || text.includes("constructor.prototype")) {
|
|
2931
|
+
violations.push(
|
|
2932
|
+
createViolation({
|
|
2933
|
+
decisionId,
|
|
2934
|
+
constraintId: constraint.id,
|
|
2935
|
+
type: constraint.type,
|
|
2936
|
+
severity: constraint.severity,
|
|
2937
|
+
message: "Potential prototype pollution pattern detected",
|
|
2938
|
+
file: filePath,
|
|
2939
|
+
line: 1,
|
|
2940
|
+
suggestion: "Avoid writing to __proto__/prototype; validate object keys"
|
|
2941
|
+
})
|
|
2942
|
+
);
|
|
2890
2943
|
}
|
|
2891
2944
|
}
|
|
2892
|
-
return
|
|
2893
|
-
}
|
|
2894
|
-
/**
|
|
2895
|
-
* Get all registered plugin IDs
|
|
2896
|
-
*/
|
|
2897
|
-
getPluginIds() {
|
|
2898
|
-
return Array.from(this.plugins.keys());
|
|
2899
|
-
}
|
|
2900
|
-
/**
|
|
2901
|
-
* Check if plugins have been loaded
|
|
2902
|
-
*/
|
|
2903
|
-
isLoaded() {
|
|
2904
|
-
return this.loaded;
|
|
2905
|
-
}
|
|
2906
|
-
/**
|
|
2907
|
-
* Get load errors (for diagnostics)
|
|
2908
|
-
*/
|
|
2909
|
-
getLoadErrors() {
|
|
2910
|
-
return [...this.loadErrors];
|
|
2911
|
-
}
|
|
2912
|
-
/**
|
|
2913
|
-
* Reset the loader (useful for testing)
|
|
2914
|
-
*/
|
|
2915
|
-
reset() {
|
|
2916
|
-
this.plugins.clear();
|
|
2917
|
-
this.loaded = false;
|
|
2918
|
-
this.loadErrors = [];
|
|
2945
|
+
return violations;
|
|
2919
2946
|
}
|
|
2920
2947
|
};
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2948
|
+
|
|
2949
|
+
// src/verification/verifiers/api.ts
|
|
2950
|
+
import { SyntaxKind as SyntaxKind4 } from "ts-morph";
|
|
2951
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
2952
|
+
function isKebabPath(pathValue) {
|
|
2953
|
+
const parts = pathValue.split("/").filter(Boolean);
|
|
2954
|
+
for (const part of parts) {
|
|
2955
|
+
if (part.startsWith(":")) continue;
|
|
2956
|
+
if (!/^[a-z0-9-]+$/.test(part)) return false;
|
|
2925
2957
|
}
|
|
2926
|
-
return
|
|
2958
|
+
return true;
|
|
2927
2959
|
}
|
|
2960
|
+
var ApiVerifier = class {
|
|
2961
|
+
id = "api";
|
|
2962
|
+
name = "API Consistency Verifier";
|
|
2963
|
+
description = "Checks basic REST endpoint naming conventions in common frameworks";
|
|
2964
|
+
async verify(ctx) {
|
|
2965
|
+
const violations = [];
|
|
2966
|
+
const { sourceFile, constraint, decisionId, filePath } = ctx;
|
|
2967
|
+
const rule = constraint.rule.toLowerCase();
|
|
2968
|
+
const enforceKebab = rule.includes("kebab") || rule.includes("kebab-case");
|
|
2969
|
+
if (!enforceKebab) return violations;
|
|
2970
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
2971
|
+
const expr = call.getExpression();
|
|
2972
|
+
const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
|
|
2973
|
+
if (!propertyAccess) continue;
|
|
2974
|
+
const method = propertyAccess.getName();
|
|
2975
|
+
if (!method || !HTTP_METHODS.has(String(method))) continue;
|
|
2976
|
+
const firstArg = call.getArguments()[0];
|
|
2977
|
+
const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
|
|
2978
|
+
if (!stringLiteral) continue;
|
|
2979
|
+
const pathValue = stringLiteral.getLiteralValue();
|
|
2980
|
+
if (typeof pathValue !== "string") continue;
|
|
2981
|
+
if (!isKebabPath(pathValue)) {
|
|
2982
|
+
violations.push(
|
|
2983
|
+
createViolation({
|
|
2984
|
+
decisionId,
|
|
2985
|
+
constraintId: constraint.id,
|
|
2986
|
+
type: constraint.type,
|
|
2987
|
+
severity: constraint.severity,
|
|
2988
|
+
message: `Endpoint path "${pathValue}" is not kebab-case`,
|
|
2989
|
+
file: filePath,
|
|
2990
|
+
line: call.getStartLineNumber(),
|
|
2991
|
+
suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
|
|
2992
|
+
})
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return violations;
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2928
2999
|
|
|
2929
3000
|
// src/verification/verifiers/index.ts
|
|
2930
3001
|
var builtinVerifiers = {
|
|
@@ -3000,51 +3071,216 @@ function selectVerifierForConstraint(rule, specifiedVerifier, check) {
|
|
|
3000
3071
|
return getVerifier("regex");
|
|
3001
3072
|
}
|
|
3002
3073
|
|
|
3003
|
-
// src/verification/
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3074
|
+
// src/verification/file-verifier.ts
|
|
3075
|
+
async function verifySingleFile(options, dependencies) {
|
|
3076
|
+
const { filePath, decisions, severityFilter, cwd, reporter, signal } = options;
|
|
3077
|
+
const { project, astCache, resultsCache, logger: logger2 } = dependencies;
|
|
3078
|
+
const violations = [];
|
|
3079
|
+
const warnings = [];
|
|
3080
|
+
const errors = [];
|
|
3081
|
+
if (signal?.aborted) {
|
|
3082
|
+
return { violations, warnings, errors };
|
|
3083
|
+
}
|
|
3084
|
+
const sourceFile = await astCache.get(filePath, project);
|
|
3085
|
+
if (!sourceFile) {
|
|
3086
|
+
return { violations, warnings, errors };
|
|
3087
|
+
}
|
|
3088
|
+
let fileHash = null;
|
|
3089
|
+
try {
|
|
3090
|
+
const content = await readFile3(filePath, "utf-8");
|
|
3091
|
+
fileHash = createHash2("sha256").update(content).digest("hex");
|
|
3092
|
+
} catch {
|
|
3093
|
+
fileHash = null;
|
|
3094
|
+
}
|
|
3095
|
+
for (const decision of decisions) {
|
|
3096
|
+
for (const constraint of decision.constraints) {
|
|
3097
|
+
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
|
|
3098
|
+
if (reporter) {
|
|
3099
|
+
reporter.add({
|
|
3100
|
+
file: filePath,
|
|
3101
|
+
decision,
|
|
3102
|
+
constraint,
|
|
3103
|
+
applied: false,
|
|
3104
|
+
reason: "File does not match scope pattern or severity filter"
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
continue;
|
|
3014
3108
|
}
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3109
|
+
const verifier = selectVerifierForConstraint(
|
|
3110
|
+
constraint.rule,
|
|
3111
|
+
constraint.verifier,
|
|
3112
|
+
constraint.check
|
|
3113
|
+
);
|
|
3114
|
+
if (!verifier) {
|
|
3115
|
+
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3116
|
+
logger2.warn(
|
|
3117
|
+
{
|
|
3118
|
+
decisionId: decision.metadata.id,
|
|
3119
|
+
constraintId: constraint.id,
|
|
3120
|
+
requestedVerifier,
|
|
3121
|
+
availableVerifiers: getVerifierIds()
|
|
3122
|
+
},
|
|
3123
|
+
"No verifier found for constraint"
|
|
3124
|
+
);
|
|
3125
|
+
warnings.push({
|
|
3126
|
+
type: "missing_verifier",
|
|
3127
|
+
message: `No verifier found for constraint (requested: ${requestedVerifier})`,
|
|
3128
|
+
decisionId: decision.metadata.id,
|
|
3129
|
+
constraintId: constraint.id,
|
|
3130
|
+
file: filePath
|
|
3131
|
+
});
|
|
3132
|
+
if (reporter) {
|
|
3133
|
+
reporter.add({
|
|
3134
|
+
file: filePath,
|
|
3135
|
+
decision,
|
|
3136
|
+
constraint,
|
|
3137
|
+
applied: false,
|
|
3138
|
+
reason: `No verifier found (requested: ${requestedVerifier})`
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
let constraintViolations;
|
|
3144
|
+
if (fileHash) {
|
|
3145
|
+
const cacheKey = {
|
|
3146
|
+
filePath,
|
|
3147
|
+
decisionId: decision.metadata.id,
|
|
3148
|
+
constraintId: constraint.id,
|
|
3149
|
+
fileHash
|
|
3150
|
+
};
|
|
3151
|
+
const cached = resultsCache.get(cacheKey);
|
|
3152
|
+
if (cached) {
|
|
3153
|
+
constraintViolations = cached;
|
|
3154
|
+
violations.push(...constraintViolations);
|
|
3155
|
+
if (reporter) {
|
|
3156
|
+
reporter.add({
|
|
3157
|
+
file: filePath,
|
|
3158
|
+
decision,
|
|
3159
|
+
constraint,
|
|
3160
|
+
applied: true,
|
|
3161
|
+
reason: "Constraint matches file scope (cached)",
|
|
3162
|
+
selectedVerifier: verifier.id
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
continue;
|
|
3166
|
+
}
|
|
3020
3167
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3168
|
+
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3169
|
+
const validationResult = getPluginLoader().validateParams(
|
|
3170
|
+
constraint.check.verifier,
|
|
3171
|
+
constraint.check.params
|
|
3172
|
+
);
|
|
3173
|
+
if (!validationResult.success) {
|
|
3174
|
+
warnings.push({
|
|
3175
|
+
type: "invalid_params",
|
|
3176
|
+
message: validationResult.error,
|
|
3177
|
+
decisionId: decision.metadata.id,
|
|
3178
|
+
constraintId: constraint.id,
|
|
3179
|
+
file: filePath
|
|
3180
|
+
});
|
|
3181
|
+
if (reporter) {
|
|
3182
|
+
reporter.add({
|
|
3183
|
+
file: filePath,
|
|
3184
|
+
decision,
|
|
3185
|
+
constraint,
|
|
3186
|
+
applied: false,
|
|
3187
|
+
reason: `Params validation failed: ${validationResult.error}`
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3026
3192
|
}
|
|
3027
|
-
|
|
3193
|
+
const verificationContext = {
|
|
3194
|
+
filePath,
|
|
3028
3195
|
sourceFile,
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3196
|
+
constraint,
|
|
3197
|
+
decisionId: decision.metadata.id,
|
|
3198
|
+
signal
|
|
3199
|
+
};
|
|
3200
|
+
const verificationStart = Date.now();
|
|
3201
|
+
try {
|
|
3202
|
+
constraintViolations = await verifier.verify(verificationContext);
|
|
3203
|
+
violations.push(...constraintViolations);
|
|
3204
|
+
if (fileHash) {
|
|
3205
|
+
resultsCache.set(
|
|
3206
|
+
{
|
|
3207
|
+
filePath,
|
|
3208
|
+
decisionId: decision.metadata.id,
|
|
3209
|
+
constraintId: constraint.id,
|
|
3210
|
+
fileHash
|
|
3211
|
+
},
|
|
3212
|
+
constraintViolations
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
if (reporter) {
|
|
3216
|
+
reporter.add({
|
|
3217
|
+
file: filePath,
|
|
3218
|
+
decision,
|
|
3219
|
+
constraint,
|
|
3220
|
+
applied: true,
|
|
3221
|
+
reason: "Constraint matches file scope",
|
|
3222
|
+
selectedVerifier: verifier.id,
|
|
3223
|
+
verifierOutput: {
|
|
3224
|
+
violations: constraintViolations.length,
|
|
3225
|
+
duration: Date.now() - verificationStart
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
} catch (error) {
|
|
3230
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3231
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3232
|
+
logger2.error(
|
|
3233
|
+
{
|
|
3234
|
+
verifierId: verifier.id,
|
|
3235
|
+
filePath,
|
|
3236
|
+
decisionId: decision.metadata.id,
|
|
3237
|
+
constraintId: constraint.id,
|
|
3238
|
+
error: errorMessage,
|
|
3239
|
+
stack: errorStack
|
|
3240
|
+
},
|
|
3241
|
+
"Verifier execution failed"
|
|
3242
|
+
);
|
|
3243
|
+
errors.push({
|
|
3244
|
+
type: "verifier_exception",
|
|
3245
|
+
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
3246
|
+
decisionId: decision.metadata.id,
|
|
3247
|
+
constraintId: constraint.id,
|
|
3248
|
+
file: filePath,
|
|
3249
|
+
stack: errorStack
|
|
3250
|
+
});
|
|
3251
|
+
if (reporter) {
|
|
3252
|
+
reporter.add({
|
|
3253
|
+
file: filePath,
|
|
3254
|
+
decision,
|
|
3255
|
+
constraint,
|
|
3256
|
+
applied: true,
|
|
3257
|
+
reason: "Constraint matches file scope",
|
|
3258
|
+
selectedVerifier: verifier.id,
|
|
3259
|
+
verifierOutput: {
|
|
3260
|
+
violations: 0,
|
|
3261
|
+
duration: Date.now() - verificationStart,
|
|
3262
|
+
error: errorMessage
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3035
3267
|
}
|
|
3036
3268
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3269
|
+
return { violations, warnings, errors };
|
|
3270
|
+
}
|
|
3271
|
+
async function verifyFilesInBatches(options) {
|
|
3272
|
+
const { files, signal, verifyFile, onFileVerified, batchSize = 50 } = options;
|
|
3273
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
3274
|
+
if (signal.aborted) {
|
|
3275
|
+
break;
|
|
3276
|
+
}
|
|
3277
|
+
const batch = files.slice(i, i + batchSize);
|
|
3278
|
+
const results = await Promise.all(batch.map((filePath) => verifyFile(filePath)));
|
|
3279
|
+
for (const result of results) {
|
|
3280
|
+
onFileVerified(result);
|
|
3281
|
+
}
|
|
3046
3282
|
}
|
|
3047
|
-
}
|
|
3283
|
+
}
|
|
3048
3284
|
|
|
3049
3285
|
// src/verification/results-cache.ts
|
|
3050
3286
|
var ResultsCache = class {
|
|
@@ -3116,30 +3352,63 @@ var ResultsCache = class {
|
|
|
3116
3352
|
}
|
|
3117
3353
|
};
|
|
3118
3354
|
|
|
3119
|
-
// src/verification/
|
|
3120
|
-
function
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3355
|
+
// src/verification/run-settings.ts
|
|
3356
|
+
function resolveVerificationRunOptions(config, options) {
|
|
3357
|
+
const level = options.level || "full";
|
|
3358
|
+
const levelConfig = config.verification?.levels?.[level];
|
|
3359
|
+
const severityFilter = options.severity || levelConfig?.severity;
|
|
3360
|
+
const timeout = options.timeout || levelConfig?.timeout || 6e4;
|
|
3361
|
+
return {
|
|
3362
|
+
level,
|
|
3363
|
+
specificFiles: options.files,
|
|
3364
|
+
decisionIds: options.decisions,
|
|
3365
|
+
severityFilter,
|
|
3366
|
+
timeout,
|
|
3367
|
+
cwd: options.cwd || process.cwd()
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
function selectDecisionsForRun(decisions, decisionIds) {
|
|
3371
|
+
if (!decisionIds || decisionIds.length === 0) {
|
|
3372
|
+
return decisions;
|
|
3373
|
+
}
|
|
3374
|
+
return decisions.filter((decision) => decisionIds.includes(decision.metadata.id));
|
|
3375
|
+
}
|
|
3376
|
+
async function resolveFilesForRun(config, specificFiles, cwd) {
|
|
3377
|
+
if (specificFiles) {
|
|
3378
|
+
return specificFiles;
|
|
3379
|
+
}
|
|
3380
|
+
return glob(config.project.sourceRoots, {
|
|
3381
|
+
cwd,
|
|
3382
|
+
ignore: config.project.exclude,
|
|
3383
|
+
absolute: true
|
|
3130
3384
|
});
|
|
3131
3385
|
}
|
|
3132
|
-
function
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3386
|
+
function createEmptyRunResult(startTime) {
|
|
3387
|
+
return {
|
|
3388
|
+
success: true,
|
|
3389
|
+
violations: [],
|
|
3390
|
+
checked: 0,
|
|
3391
|
+
passed: 0,
|
|
3392
|
+
failed: 0,
|
|
3393
|
+
skipped: 0,
|
|
3394
|
+
duration: Date.now() - startTime,
|
|
3395
|
+
warnings: [],
|
|
3396
|
+
errors: []
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
function hasBlockingViolations(violations, level) {
|
|
3400
|
+
return violations.some((violation) => {
|
|
3401
|
+
if (level === "commit") {
|
|
3402
|
+
return violation.type === "invariant" || violation.severity === "critical";
|
|
3403
|
+
}
|
|
3404
|
+
if (level === "pr") {
|
|
3405
|
+
return violation.type === "invariant" || violation.severity === "critical" || violation.severity === "high";
|
|
3406
|
+
}
|
|
3407
|
+
return violation.type === "invariant";
|
|
3408
|
+
});
|
|
3138
3409
|
}
|
|
3139
3410
|
|
|
3140
3411
|
// src/verification/engine.ts
|
|
3141
|
-
import { createHash as createHash2 } from "crypto";
|
|
3142
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
3143
3412
|
var VerificationEngine = class {
|
|
3144
3413
|
registry;
|
|
3145
3414
|
project;
|
|
@@ -3166,333 +3435,90 @@ var VerificationEngine = class {
|
|
|
3166
3435
|
*/
|
|
3167
3436
|
async verify(config, options = {}) {
|
|
3168
3437
|
const startTime = Date.now();
|
|
3169
|
-
const
|
|
3170
|
-
|
|
3171
|
-
files: specificFiles,
|
|
3172
|
-
decisions: decisionIds,
|
|
3173
|
-
cwd = process.cwd()
|
|
3174
|
-
} = options;
|
|
3175
|
-
const levelConfig = config.verification?.levels?.[level];
|
|
3176
|
-
const severityFilter = options.severity || levelConfig?.severity;
|
|
3177
|
-
const timeout = options.timeout || levelConfig?.timeout || 6e4;
|
|
3178
|
-
if (!this.pluginsLoaded) {
|
|
3179
|
-
await getPluginLoader().loadPlugins(cwd);
|
|
3180
|
-
this.pluginsLoaded = true;
|
|
3181
|
-
}
|
|
3438
|
+
const settings = resolveVerificationRunOptions(config, options);
|
|
3439
|
+
await this.ensurePluginsLoaded(settings.cwd);
|
|
3182
3440
|
await this.registry.load();
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
decisions = decisions.filter((d) => decisionIds.includes(d.metadata.id));
|
|
3186
|
-
}
|
|
3187
|
-
const filesToVerify = specificFiles ? specificFiles : await glob(config.project.sourceRoots, {
|
|
3188
|
-
cwd,
|
|
3189
|
-
ignore: config.project.exclude,
|
|
3190
|
-
absolute: true
|
|
3191
|
-
});
|
|
3441
|
+
const decisions = selectDecisionsForRun(this.registry.getActive(), settings.decisionIds);
|
|
3442
|
+
const filesToVerify = await resolveFilesForRun(config, settings.specificFiles, settings.cwd);
|
|
3192
3443
|
if (filesToVerify.length === 0) {
|
|
3193
|
-
return
|
|
3194
|
-
success: true,
|
|
3195
|
-
violations: [],
|
|
3196
|
-
checked: 0,
|
|
3197
|
-
passed: 0,
|
|
3198
|
-
failed: 0,
|
|
3199
|
-
skipped: 0,
|
|
3200
|
-
duration: Date.now() - startTime,
|
|
3201
|
-
warnings: [],
|
|
3202
|
-
errors: []
|
|
3203
|
-
};
|
|
3444
|
+
return createEmptyRunResult(startTime);
|
|
3204
3445
|
}
|
|
3205
|
-
const
|
|
3206
|
-
const allWarnings = [];
|
|
3207
|
-
const allErrors = [];
|
|
3208
|
-
let checked = 0;
|
|
3209
|
-
let passed = 0;
|
|
3210
|
-
let failed = 0;
|
|
3211
|
-
const skipped = 0;
|
|
3446
|
+
const accumulator = this.createAccumulator();
|
|
3212
3447
|
const abortController = new AbortController();
|
|
3213
3448
|
let timeoutHandle = null;
|
|
3214
3449
|
const timeoutPromise = new Promise((resolve2) => {
|
|
3215
3450
|
timeoutHandle = setTimeout(() => {
|
|
3216
3451
|
abortController.abort();
|
|
3217
3452
|
resolve2("timeout");
|
|
3218
|
-
}, timeout);
|
|
3453
|
+
}, settings.timeout);
|
|
3219
3454
|
timeoutHandle.unref();
|
|
3220
3455
|
});
|
|
3221
|
-
const verificationPromise =
|
|
3222
|
-
filesToVerify,
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
} else {
|
|
3236
|
-
passed++;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
);
|
|
3240
|
-
let result;
|
|
3456
|
+
const verificationPromise = verifyFilesInBatches({
|
|
3457
|
+
files: filesToVerify,
|
|
3458
|
+
signal: abortController.signal,
|
|
3459
|
+
verifyFile: (filePath) => this.verifyFile(
|
|
3460
|
+
filePath,
|
|
3461
|
+
decisions,
|
|
3462
|
+
settings.severityFilter,
|
|
3463
|
+
settings.cwd,
|
|
3464
|
+
options.reporter,
|
|
3465
|
+
abortController.signal
|
|
3466
|
+
),
|
|
3467
|
+
onFileVerified: (result) => this.addFileResult(accumulator, result)
|
|
3468
|
+
});
|
|
3469
|
+
let raceResult;
|
|
3241
3470
|
try {
|
|
3242
|
-
|
|
3243
|
-
if (result === "timeout") {
|
|
3244
|
-
return {
|
|
3245
|
-
success: false,
|
|
3246
|
-
violations: allViolations,
|
|
3247
|
-
checked,
|
|
3248
|
-
passed,
|
|
3249
|
-
failed,
|
|
3250
|
-
skipped: filesToVerify.length - checked,
|
|
3251
|
-
duration: timeout,
|
|
3252
|
-
warnings: allWarnings,
|
|
3253
|
-
errors: allErrors
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3471
|
+
raceResult = await Promise.race([verificationPromise, timeoutPromise]);
|
|
3256
3472
|
} finally {
|
|
3257
3473
|
if (timeoutHandle) {
|
|
3258
3474
|
clearTimeout(timeoutHandle);
|
|
3259
|
-
timeoutHandle = null;
|
|
3260
3475
|
}
|
|
3261
3476
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3477
|
+
if (raceResult === "timeout") {
|
|
3478
|
+
return {
|
|
3479
|
+
success: false,
|
|
3480
|
+
violations: accumulator.violations,
|
|
3481
|
+
checked: accumulator.checked,
|
|
3482
|
+
passed: accumulator.passed,
|
|
3483
|
+
failed: accumulator.failed,
|
|
3484
|
+
skipped: filesToVerify.length - accumulator.checked,
|
|
3485
|
+
duration: settings.timeout,
|
|
3486
|
+
warnings: accumulator.warnings,
|
|
3487
|
+
errors: accumulator.errors
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3271
3490
|
return {
|
|
3272
|
-
success: !hasBlockingViolations,
|
|
3273
|
-
violations:
|
|
3274
|
-
checked,
|
|
3275
|
-
passed,
|
|
3276
|
-
failed,
|
|
3277
|
-
skipped,
|
|
3491
|
+
success: !hasBlockingViolations(accumulator.violations, settings.level),
|
|
3492
|
+
violations: accumulator.violations,
|
|
3493
|
+
checked: accumulator.checked,
|
|
3494
|
+
passed: accumulator.passed,
|
|
3495
|
+
failed: accumulator.failed,
|
|
3496
|
+
skipped: 0,
|
|
3278
3497
|
duration: Date.now() - startTime,
|
|
3279
|
-
warnings:
|
|
3280
|
-
errors:
|
|
3498
|
+
warnings: accumulator.warnings,
|
|
3499
|
+
errors: accumulator.errors
|
|
3281
3500
|
};
|
|
3282
3501
|
}
|
|
3283
3502
|
/**
|
|
3284
3503
|
* Verify a single file
|
|
3285
3504
|
*/
|
|
3286
3505
|
async verifyFile(filePath, decisions, severityFilter, cwd = process.cwd(), reporter, signal) {
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
}
|
|
3302
|
-
for (const decision of decisions) {
|
|
3303
|
-
for (const constraint of decision.constraints) {
|
|
3304
|
-
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd, severityFilter })) {
|
|
3305
|
-
if (reporter) {
|
|
3306
|
-
reporter.add({
|
|
3307
|
-
file: filePath,
|
|
3308
|
-
decision,
|
|
3309
|
-
constraint,
|
|
3310
|
-
applied: false,
|
|
3311
|
-
reason: "File does not match scope pattern or severity filter"
|
|
3312
|
-
});
|
|
3313
|
-
}
|
|
3314
|
-
continue;
|
|
3315
|
-
}
|
|
3316
|
-
const verifier = selectVerifierForConstraint(
|
|
3317
|
-
constraint.rule,
|
|
3318
|
-
constraint.verifier,
|
|
3319
|
-
constraint.check
|
|
3320
|
-
);
|
|
3321
|
-
if (!verifier) {
|
|
3322
|
-
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3323
|
-
this.logger.warn(
|
|
3324
|
-
{
|
|
3325
|
-
decisionId: decision.metadata.id,
|
|
3326
|
-
constraintId: constraint.id,
|
|
3327
|
-
requestedVerifier,
|
|
3328
|
-
availableVerifiers: getVerifierIds()
|
|
3329
|
-
},
|
|
3330
|
-
"No verifier found for constraint"
|
|
3331
|
-
);
|
|
3332
|
-
warnings.push({
|
|
3333
|
-
type: "missing_verifier",
|
|
3334
|
-
message: `No verifier found for constraint (requested: ${requestedVerifier})`,
|
|
3335
|
-
decisionId: decision.metadata.id,
|
|
3336
|
-
constraintId: constraint.id,
|
|
3337
|
-
file: filePath
|
|
3338
|
-
});
|
|
3339
|
-
if (reporter) {
|
|
3340
|
-
reporter.add({
|
|
3341
|
-
file: filePath,
|
|
3342
|
-
decision,
|
|
3343
|
-
constraint,
|
|
3344
|
-
applied: false,
|
|
3345
|
-
reason: `No verifier found (requested: ${requestedVerifier})`
|
|
3346
|
-
});
|
|
3347
|
-
}
|
|
3348
|
-
continue;
|
|
3349
|
-
}
|
|
3350
|
-
let constraintViolations;
|
|
3351
|
-
if (fileHash) {
|
|
3352
|
-
const cacheKey = {
|
|
3353
|
-
filePath,
|
|
3354
|
-
decisionId: decision.metadata.id,
|
|
3355
|
-
constraintId: constraint.id,
|
|
3356
|
-
fileHash
|
|
3357
|
-
};
|
|
3358
|
-
const cached = this.resultsCache.get(cacheKey);
|
|
3359
|
-
if (cached) {
|
|
3360
|
-
constraintViolations = cached;
|
|
3361
|
-
violations.push(...constraintViolations);
|
|
3362
|
-
if (reporter) {
|
|
3363
|
-
reporter.add({
|
|
3364
|
-
file: filePath,
|
|
3365
|
-
decision,
|
|
3366
|
-
constraint,
|
|
3367
|
-
applied: true,
|
|
3368
|
-
reason: "Constraint matches file scope (cached)",
|
|
3369
|
-
selectedVerifier: verifier.id
|
|
3370
|
-
});
|
|
3371
|
-
}
|
|
3372
|
-
continue;
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3376
|
-
const pluginLoader2 = getPluginLoader();
|
|
3377
|
-
const validationResult = pluginLoader2.validateParams(
|
|
3378
|
-
constraint.check.verifier,
|
|
3379
|
-
constraint.check.params
|
|
3380
|
-
);
|
|
3381
|
-
if (!validationResult.success) {
|
|
3382
|
-
warnings.push({
|
|
3383
|
-
type: "invalid_params",
|
|
3384
|
-
message: validationResult.error,
|
|
3385
|
-
decisionId: decision.metadata.id,
|
|
3386
|
-
constraintId: constraint.id,
|
|
3387
|
-
file: filePath
|
|
3388
|
-
});
|
|
3389
|
-
if (reporter) {
|
|
3390
|
-
reporter.add({
|
|
3391
|
-
file: filePath,
|
|
3392
|
-
decision,
|
|
3393
|
-
constraint,
|
|
3394
|
-
applied: false,
|
|
3395
|
-
reason: `Params validation failed: ${validationResult.error}`
|
|
3396
|
-
});
|
|
3397
|
-
}
|
|
3398
|
-
continue;
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
const ctx = {
|
|
3402
|
-
filePath,
|
|
3403
|
-
sourceFile,
|
|
3404
|
-
constraint,
|
|
3405
|
-
decisionId: decision.metadata.id,
|
|
3406
|
-
signal
|
|
3407
|
-
};
|
|
3408
|
-
const verificationStart = Date.now();
|
|
3409
|
-
try {
|
|
3410
|
-
constraintViolations = await verifier.verify(ctx);
|
|
3411
|
-
violations.push(...constraintViolations);
|
|
3412
|
-
if (fileHash) {
|
|
3413
|
-
this.resultsCache.set(
|
|
3414
|
-
{
|
|
3415
|
-
filePath,
|
|
3416
|
-
decisionId: decision.metadata.id,
|
|
3417
|
-
constraintId: constraint.id,
|
|
3418
|
-
fileHash
|
|
3419
|
-
},
|
|
3420
|
-
constraintViolations
|
|
3421
|
-
);
|
|
3422
|
-
}
|
|
3423
|
-
if (reporter) {
|
|
3424
|
-
reporter.add({
|
|
3425
|
-
file: filePath,
|
|
3426
|
-
decision,
|
|
3427
|
-
constraint,
|
|
3428
|
-
applied: true,
|
|
3429
|
-
reason: "Constraint matches file scope",
|
|
3430
|
-
selectedVerifier: verifier.id,
|
|
3431
|
-
verifierOutput: {
|
|
3432
|
-
violations: constraintViolations.length,
|
|
3433
|
-
duration: Date.now() - verificationStart
|
|
3434
|
-
}
|
|
3435
|
-
});
|
|
3436
|
-
}
|
|
3437
|
-
} catch (error) {
|
|
3438
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3439
|
-
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3440
|
-
this.logger.error(
|
|
3441
|
-
{
|
|
3442
|
-
verifierId: verifier.id,
|
|
3443
|
-
filePath,
|
|
3444
|
-
decisionId: decision.metadata.id,
|
|
3445
|
-
constraintId: constraint.id,
|
|
3446
|
-
error: errorMessage,
|
|
3447
|
-
stack: errorStack
|
|
3448
|
-
},
|
|
3449
|
-
"Verifier execution failed"
|
|
3450
|
-
);
|
|
3451
|
-
errors.push({
|
|
3452
|
-
type: "verifier_exception",
|
|
3453
|
-
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
3454
|
-
decisionId: decision.metadata.id,
|
|
3455
|
-
constraintId: constraint.id,
|
|
3456
|
-
file: filePath,
|
|
3457
|
-
stack: errorStack
|
|
3458
|
-
});
|
|
3459
|
-
if (reporter) {
|
|
3460
|
-
reporter.add({
|
|
3461
|
-
file: filePath,
|
|
3462
|
-
decision,
|
|
3463
|
-
constraint,
|
|
3464
|
-
applied: true,
|
|
3465
|
-
reason: "Constraint matches file scope",
|
|
3466
|
-
selectedVerifier: verifier.id,
|
|
3467
|
-
verifierOutput: {
|
|
3468
|
-
violations: 0,
|
|
3469
|
-
duration: Date.now() - verificationStart,
|
|
3470
|
-
error: errorMessage
|
|
3471
|
-
}
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
}
|
|
3476
|
-
}
|
|
3477
|
-
return { violations, warnings, errors };
|
|
3478
|
-
}
|
|
3479
|
-
/**
|
|
3480
|
-
* Verify multiple files
|
|
3481
|
-
*/
|
|
3482
|
-
async verifyFiles(files, decisions, severityFilter, cwd, reporter, signal, onFileVerified) {
|
|
3483
|
-
const BATCH_SIZE = 50;
|
|
3484
|
-
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
3485
|
-
if (signal.aborted) {
|
|
3486
|
-
break;
|
|
3487
|
-
}
|
|
3488
|
-
const batch = files.slice(i, i + BATCH_SIZE);
|
|
3489
|
-
const results = await Promise.all(
|
|
3490
|
-
batch.map((file) => this.verifyFile(file, decisions, severityFilter, cwd, reporter, signal))
|
|
3491
|
-
);
|
|
3492
|
-
for (const result of results) {
|
|
3493
|
-
onFileVerified(result.violations, result.warnings, result.errors);
|
|
3506
|
+
return verifySingleFile(
|
|
3507
|
+
{
|
|
3508
|
+
filePath,
|
|
3509
|
+
decisions,
|
|
3510
|
+
severityFilter,
|
|
3511
|
+
cwd,
|
|
3512
|
+
reporter,
|
|
3513
|
+
signal
|
|
3514
|
+
},
|
|
3515
|
+
{
|
|
3516
|
+
project: this.project,
|
|
3517
|
+
astCache: this.astCache,
|
|
3518
|
+
resultsCache: this.resultsCache,
|
|
3519
|
+
logger: this.logger
|
|
3494
3520
|
}
|
|
3495
|
-
|
|
3521
|
+
);
|
|
3496
3522
|
}
|
|
3497
3523
|
/**
|
|
3498
3524
|
* Get registry
|
|
@@ -3500,6 +3526,34 @@ var VerificationEngine = class {
|
|
|
3500
3526
|
getRegistry() {
|
|
3501
3527
|
return this.registry;
|
|
3502
3528
|
}
|
|
3529
|
+
async ensurePluginsLoaded(cwd) {
|
|
3530
|
+
if (this.pluginsLoaded) {
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
await getPluginLoader().loadPlugins(cwd);
|
|
3534
|
+
this.pluginsLoaded = true;
|
|
3535
|
+
}
|
|
3536
|
+
createAccumulator() {
|
|
3537
|
+
return {
|
|
3538
|
+
violations: [],
|
|
3539
|
+
warnings: [],
|
|
3540
|
+
errors: [],
|
|
3541
|
+
checked: 0,
|
|
3542
|
+
passed: 0,
|
|
3543
|
+
failed: 0
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
addFileResult(accumulator, result) {
|
|
3547
|
+
accumulator.violations.push(...result.violations);
|
|
3548
|
+
accumulator.warnings.push(...result.warnings);
|
|
3549
|
+
accumulator.errors.push(...result.errors);
|
|
3550
|
+
accumulator.checked++;
|
|
3551
|
+
if (result.violations.length > 0) {
|
|
3552
|
+
accumulator.failed++;
|
|
3553
|
+
} else {
|
|
3554
|
+
accumulator.passed++;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3503
3557
|
};
|
|
3504
3558
|
function createVerificationEngine(registry) {
|
|
3505
3559
|
return new VerificationEngine(registry);
|