@cleartrip/frontguard 0.2.2 → 0.2.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/cli.js +300 -4
- package/dist/cli.js.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import fs2 from 'fs';
|
|
|
13
13
|
import { pipeline } from 'stream/promises';
|
|
14
14
|
import { PassThrough } from 'stream';
|
|
15
15
|
import fg from 'fast-glob';
|
|
16
|
+
import * as ts from 'typescript';
|
|
16
17
|
|
|
17
18
|
var __create = Object.create;
|
|
18
19
|
var __defProp = Object.defineProperty;
|
|
@@ -2953,9 +2954,9 @@ async function detectStack(cwd) {
|
|
|
2953
2954
|
try {
|
|
2954
2955
|
const tsconfigPath = path5.join(cwd, "tsconfig.json");
|
|
2955
2956
|
const tsRaw = await fs.readFile(tsconfigPath, "utf8");
|
|
2956
|
-
const
|
|
2957
|
-
if (typeof
|
|
2958
|
-
tsStrict =
|
|
2957
|
+
const ts2 = JSON.parse(tsRaw);
|
|
2958
|
+
if (typeof ts2.compilerOptions?.strict === "boolean") {
|
|
2959
|
+
tsStrict = ts2.compilerOptions.strict;
|
|
2959
2960
|
}
|
|
2960
2961
|
} catch {
|
|
2961
2962
|
}
|
|
@@ -5013,6 +5014,178 @@ async function runCustomRules(cwd, config, restrictToFiles) {
|
|
|
5013
5014
|
durationMs: Math.round(performance.now() - t0)
|
|
5014
5015
|
};
|
|
5015
5016
|
}
|
|
5017
|
+
var BIG_LITERAL_MIN = 9e3;
|
|
5018
|
+
function scanDiscardedExpensiveCalls(regionText, fileNameHint, regionStartLine) {
|
|
5019
|
+
const kind = /\.(tsx|jsx)$/i.test(fileNameHint) ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
5020
|
+
const sf = ts.createSourceFile(
|
|
5021
|
+
fileNameHint,
|
|
5022
|
+
regionText,
|
|
5023
|
+
ts.ScriptTarget.Latest,
|
|
5024
|
+
true,
|
|
5025
|
+
kind
|
|
5026
|
+
);
|
|
5027
|
+
const parseDiags = sf.parseDiagnostics;
|
|
5028
|
+
if (parseDiags && parseDiags.length > 12) {
|
|
5029
|
+
return [];
|
|
5030
|
+
}
|
|
5031
|
+
const stack = [/* @__PURE__ */ new Map()];
|
|
5032
|
+
const lines = [];
|
|
5033
|
+
function current() {
|
|
5034
|
+
return stack[stack.length - 1];
|
|
5035
|
+
}
|
|
5036
|
+
function lookupExpensive(name) {
|
|
5037
|
+
for (let i3 = stack.length - 1; i3 >= 0; i3--) {
|
|
5038
|
+
const v3 = stack[i3].get(name);
|
|
5039
|
+
if (v3 !== void 0) return v3;
|
|
5040
|
+
}
|
|
5041
|
+
return false;
|
|
5042
|
+
}
|
|
5043
|
+
function lineOf(node) {
|
|
5044
|
+
const lc = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf, false));
|
|
5045
|
+
return regionStartLine + lc.line;
|
|
5046
|
+
}
|
|
5047
|
+
function visitFunctionLike(fn) {
|
|
5048
|
+
stack.push(/* @__PURE__ */ new Map());
|
|
5049
|
+
for (const p2 of fn.parameters) {
|
|
5050
|
+
if (ts.isIdentifier(p2.name)) current().set(p2.name.text, false);
|
|
5051
|
+
}
|
|
5052
|
+
const body = fn.body;
|
|
5053
|
+
if (!body) {
|
|
5054
|
+
stack.pop();
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
if (ts.isBlock(body)) {
|
|
5058
|
+
for (const st of body.statements) visitStmt(st);
|
|
5059
|
+
} else if (ts.isExpression(body)) {
|
|
5060
|
+
visitStmt(ts.factory.createExpressionStatement(body));
|
|
5061
|
+
}
|
|
5062
|
+
stack.pop();
|
|
5063
|
+
}
|
|
5064
|
+
function visitBlock(block) {
|
|
5065
|
+
stack.push(/* @__PURE__ */ new Map());
|
|
5066
|
+
for (const st of block.statements) visitStmt(st);
|
|
5067
|
+
stack.pop();
|
|
5068
|
+
}
|
|
5069
|
+
function visitMaybeBlock(s3) {
|
|
5070
|
+
if (ts.isBlock(s3)) visitBlock(s3);
|
|
5071
|
+
else visitStmt(s3);
|
|
5072
|
+
}
|
|
5073
|
+
function visitStmt(st) {
|
|
5074
|
+
if (ts.isVariableStatement(st)) {
|
|
5075
|
+
for (const decl of st.declarationList.declarations) {
|
|
5076
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
5077
|
+
const name = decl.name.text;
|
|
5078
|
+
const init3 = decl.initializer;
|
|
5079
|
+
if (ts.isArrowFunction(init3) || ts.isFunctionExpression(init3)) {
|
|
5080
|
+
current().set(name, isBodyExpensive(init3.body));
|
|
5081
|
+
visitFunctionLike(init3);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
return;
|
|
5085
|
+
}
|
|
5086
|
+
if (ts.isFunctionDeclaration(st) && st.name && st.body) {
|
|
5087
|
+
current().set(st.name.text, isBodyExpensive(st.body));
|
|
5088
|
+
visitFunctionLike(st);
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
if (ts.isExpressionStatement(st)) {
|
|
5092
|
+
const ex = st.expression;
|
|
5093
|
+
if (ts.isCallExpression(ex) && !ex.questionDotToken && ts.isIdentifier(ex.expression) && lookupExpensive(ex.expression.text)) {
|
|
5094
|
+
lines.push(lineOf(ex));
|
|
5095
|
+
}
|
|
5096
|
+
return;
|
|
5097
|
+
}
|
|
5098
|
+
if (ts.isBlock(st)) {
|
|
5099
|
+
visitBlock(st);
|
|
5100
|
+
return;
|
|
5101
|
+
}
|
|
5102
|
+
if (ts.isIfStatement(st)) {
|
|
5103
|
+
visitMaybeBlock(st.thenStatement);
|
|
5104
|
+
if (st.elseStatement) visitMaybeBlock(st.elseStatement);
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
if (ts.isForStatement(st) || ts.isForOfStatement(st) || ts.isForInStatement(st) || ts.isWhileStatement(st) || ts.isDoStatement(st)) {
|
|
5108
|
+
visitMaybeBlock(st.statement);
|
|
5109
|
+
return;
|
|
5110
|
+
}
|
|
5111
|
+
if (ts.isTryStatement(st)) {
|
|
5112
|
+
visitBlock(st.tryBlock);
|
|
5113
|
+
if (st.catchClause?.block) visitBlock(st.catchClause.block);
|
|
5114
|
+
if (st.finallyBlock) visitBlock(st.finallyBlock);
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
if (ts.isSwitchStatement(st)) {
|
|
5118
|
+
for (const clause of st.caseBlock.clauses) {
|
|
5119
|
+
for (const s3 of clause.statements) visitStmt(s3);
|
|
5120
|
+
}
|
|
5121
|
+
return;
|
|
5122
|
+
}
|
|
5123
|
+
if (ts.isClassDeclaration(st) && st.members) {
|
|
5124
|
+
for (const member of st.members) {
|
|
5125
|
+
if (ts.isMethodDeclaration(member) && member.body) {
|
|
5126
|
+
const nm = member.name;
|
|
5127
|
+
if (ts.isIdentifier(nm)) {
|
|
5128
|
+
current().set(nm.text, isBodyExpensive(member.body));
|
|
5129
|
+
}
|
|
5130
|
+
visitFunctionLike(member);
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
for (const st of sf.statements) {
|
|
5136
|
+
if (ts.isImportDeclaration(st) || ts.isImportEqualsDeclaration(st)) continue;
|
|
5137
|
+
if (ts.isExportDeclaration(st)) continue;
|
|
5138
|
+
if (ts.isExportAssignment(st)) {
|
|
5139
|
+
const ex = st.expression;
|
|
5140
|
+
if (ts.isArrowFunction(ex) || ts.isFunctionExpression(ex)) {
|
|
5141
|
+
visitFunctionLike(ex);
|
|
5142
|
+
}
|
|
5143
|
+
continue;
|
|
5144
|
+
}
|
|
5145
|
+
visitStmt(st);
|
|
5146
|
+
}
|
|
5147
|
+
return dedupeSorted(lines);
|
|
5148
|
+
}
|
|
5149
|
+
function dedupeSorted(nums) {
|
|
5150
|
+
return [...new Set(nums)].sort((a3, b3) => a3 - b3);
|
|
5151
|
+
}
|
|
5152
|
+
function isBodyExpensive(body) {
|
|
5153
|
+
if (!body) return false;
|
|
5154
|
+
if (ts.isBlock(body)) return blockHasExpensiveLoop(body);
|
|
5155
|
+
return nodeHasExpensiveLoop(body);
|
|
5156
|
+
}
|
|
5157
|
+
function blockHasExpensiveLoop(block) {
|
|
5158
|
+
return nodeHasExpensiveLoop(block);
|
|
5159
|
+
}
|
|
5160
|
+
function nodeHasExpensiveLoop(node) {
|
|
5161
|
+
let found = false;
|
|
5162
|
+
const visit = (n3) => {
|
|
5163
|
+
if (found) return;
|
|
5164
|
+
if (ts.isForStatement(n3) || ts.isForOfStatement(n3) || ts.isForInStatement(n3) || ts.isWhileStatement(n3) || ts.isDoStatement(n3)) {
|
|
5165
|
+
if (subtreeHasBigNumeric(n3, BIG_LITERAL_MIN)) found = true;
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5168
|
+
ts.forEachChild(n3, visit);
|
|
5169
|
+
};
|
|
5170
|
+
visit(node);
|
|
5171
|
+
return found;
|
|
5172
|
+
}
|
|
5173
|
+
function subtreeHasBigNumeric(node, min) {
|
|
5174
|
+
let found = false;
|
|
5175
|
+
const visit = (n3) => {
|
|
5176
|
+
if (found) return;
|
|
5177
|
+
if (ts.isNumericLiteral(n3)) {
|
|
5178
|
+
const v3 = Number(n3.text.replace(/_/g, ""));
|
|
5179
|
+
if (Number.isFinite(v3) && v3 >= min) {
|
|
5180
|
+
found = true;
|
|
5181
|
+
return;
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
ts.forEachChild(n3, visit);
|
|
5185
|
+
};
|
|
5186
|
+
visit(node);
|
|
5187
|
+
return found;
|
|
5188
|
+
}
|
|
5016
5189
|
|
|
5017
5190
|
// src/lib/ai-decorators.ts
|
|
5018
5191
|
var FILE_SCAN_HEAD_LINES = 40;
|
|
@@ -5183,6 +5356,21 @@ var PATTERNS2 = [
|
|
|
5183
5356
|
id: "ai-sql-template",
|
|
5184
5357
|
re: /(?:query|execute|raw)\s*\(\s*[`'"][^`'"]*\$\{/i,
|
|
5185
5358
|
message: "Possible dynamic SQL/string \u2014 ensure parameterization, not string concat."
|
|
5359
|
+
},
|
|
5360
|
+
/**
|
|
5361
|
+
* Classic C-style loop with `<= ...length` — often an off-by-one with `charAt(i)` / array indexing
|
|
5362
|
+
* (index `length` is out of range). Prefer `< length`, `for...of`, or `Array.from`.
|
|
5363
|
+
*/
|
|
5364
|
+
{
|
|
5365
|
+
id: "ai-for-lte-length",
|
|
5366
|
+
re: /for\s*\(\s*(?:let|var|const)\s+\w+\s*=\s*\d+\s*;\s*\w+\s*<=\s*[^;)]+\.length\s*;/,
|
|
5367
|
+
message: "Loop condition uses `<= ...length` \u2014 often an extra iteration (e.g. `charAt(length)` is empty). Prefer `< ...length` or a safer iteration style."
|
|
5368
|
+
},
|
|
5369
|
+
/** AI often pastes broad eslint suppression without review. */
|
|
5370
|
+
{
|
|
5371
|
+
id: "ai-eslint-disable",
|
|
5372
|
+
re: /eslint-disable(?:-next-line|-line)?\b/i,
|
|
5373
|
+
message: "ESLint disable in AI-marked code \u2014 confirm the rule violation is understood and cannot be fixed properly."
|
|
5186
5374
|
}
|
|
5187
5375
|
];
|
|
5188
5376
|
function scanRegion(rel, regionText, regionStartLine, gate, tag, findings) {
|
|
@@ -5198,6 +5386,16 @@ function scanRegion(rel, regionText, regionStartLine, gate, tag, findings) {
|
|
|
5198
5386
|
});
|
|
5199
5387
|
}
|
|
5200
5388
|
}
|
|
5389
|
+
const astLines = scanDiscardedExpensiveCalls(regionText, rel, regionStartLine);
|
|
5390
|
+
for (const line of astLines) {
|
|
5391
|
+
findings.push({
|
|
5392
|
+
id: "ai-discarded-expensive-call",
|
|
5393
|
+
severity: sev(gate),
|
|
5394
|
+
message: `${tag} Call discards the return value of a local function whose body includes a loop with a very large numeric bound \u2014 likely wasted CPU on every run (e.g. remove the call or move work to an effect / memo with a real dependency).`,
|
|
5395
|
+
file: rel,
|
|
5396
|
+
detail: `line ${line}`
|
|
5397
|
+
});
|
|
5398
|
+
}
|
|
5201
5399
|
}
|
|
5202
5400
|
async function runAiAssistedStrict(cwd, config, pr) {
|
|
5203
5401
|
const t0 = performance.now();
|
|
@@ -5321,6 +5519,26 @@ function escapeHtml(s3) {
|
|
|
5321
5519
|
return s3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5322
5520
|
}
|
|
5323
5521
|
|
|
5522
|
+
// src/report/check-descriptions.ts
|
|
5523
|
+
var CHECK_DESCRIPTIONS = {
|
|
5524
|
+
eslint: "Runs ESLint using your project config. Flags style, correctness, and framework-specific issues in the repo or PR-scoped files.",
|
|
5525
|
+
prettier: "Checks that files match Prettier formatting. Catches unformatted edits so the codebase stays consistent.",
|
|
5526
|
+
typescript: "Runs the TypeScript compiler (tsc --noEmit) on the project. Surfaces type errors before merge.",
|
|
5527
|
+
secrets: "Scans changed files for patterns that look like leaked secrets (tokens, keys). Heuristic \u2014 review each hit.",
|
|
5528
|
+
cycles: "Runs madge for circular dependencies on TypeScript/JavaScript entry points. Import cycles can cause brittle builds and load order bugs.",
|
|
5529
|
+
"dead-code": "Runs ts-prune to find unused exports in the TypeScript project. Helps trim dead surface area.",
|
|
5530
|
+
bundle: "Measures total size of configured build artifacts (glob) and compares to a checked-in baseline. Flags large regressions in shipped JS/CSS.",
|
|
5531
|
+
"core-web-vitals": "Static hints in JSX/TSX related to Core Web Vitals (e.g. LCP-friendly images, main-thread hygiene). Not a substitute for real field metrics.",
|
|
5532
|
+
"ai-assisted-strict": "When the PR is AI-assisted or code is marked with @frontguard-ai decorators, scans those regions for risky patterns (eval, XSS sinks, etc.) and a few AST heuristics (e.g. discarded hot loops).",
|
|
5533
|
+
"pr-hygiene": "Validates PR metadata when CI provides PR context: description length, checklist items, and similar hygiene rules from config.",
|
|
5534
|
+
"pr-size": "Compares PR diff size (lines/files) against configured budgets to discourage oversized changes.",
|
|
5535
|
+
"ts-any-delta": "Diffs the branch against a base ref and counts newly added uses of the TypeScript any type. Helps stop gradual loss of type safety.",
|
|
5536
|
+
"custom-rules": "Runs optional file/content rules you define in FrontGuard config (regex or structured checks on paths). Skipped when no rules are configured."
|
|
5537
|
+
};
|
|
5538
|
+
function getCheckDescription(checkId) {
|
|
5539
|
+
return CHECK_DESCRIPTIONS[checkId] ?? `FrontGuard check "${checkId}". See your frontguard config and docs for behavior.`;
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5324
5542
|
// src/report/html-report.ts
|
|
5325
5543
|
function parseLineHint(detail) {
|
|
5326
5544
|
if (!detail) return 0;
|
|
@@ -5386,7 +5604,10 @@ function buildHtmlReport(p2) {
|
|
|
5386
5604
|
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
5387
5605
|
const checkRows = results.map((r4) => {
|
|
5388
5606
|
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
5389
|
-
|
|
5607
|
+
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
5608
|
+
const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
|
|
5609
|
+
const checkTitle = `<span class="check-title-cell"><strong class="check-name">${escapeHtml(r4.checkId)}</strong><span class="check-info-wrap"><button type="button" class="check-info" title="${help}" aria-label="${ariaWhat}">i</button><span class="check-tooltip" role="tooltip">${help}</span></span></span>`;
|
|
5610
|
+
return `<tr><td class="td-icon">${statusDot(r4)}</td><td class="td-check">${checkTitle}</td><td class="td-status">${status}</td><td class="td-num">${r4.skipped ? "\u2014" : r4.findings.length}</td><td class="td-time">${formatDuration(r4.durationMs)}</td></tr>`;
|
|
5390
5611
|
}).join("\n");
|
|
5391
5612
|
const blockItems = sortFindings(
|
|
5392
5613
|
cwd,
|
|
@@ -5556,8 +5777,83 @@ function buildHtmlReport(p2) {
|
|
|
5556
5777
|
letter-spacing: 0.04em;
|
|
5557
5778
|
}
|
|
5558
5779
|
.td-icon { width: 2rem; vertical-align: middle; }
|
|
5780
|
+
.td-check { vertical-align: middle; }
|
|
5559
5781
|
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
5782
|
+
.check-title-cell {
|
|
5783
|
+
display: inline-flex;
|
|
5784
|
+
align-items: center;
|
|
5785
|
+
gap: 0.35rem;
|
|
5786
|
+
flex-wrap: nowrap;
|
|
5787
|
+
}
|
|
5560
5788
|
.check-name { font-weight: 600; }
|
|
5789
|
+
.check-info-wrap {
|
|
5790
|
+
position: relative;
|
|
5791
|
+
display: inline-flex;
|
|
5792
|
+
align-items: center;
|
|
5793
|
+
flex-shrink: 0;
|
|
5794
|
+
}
|
|
5795
|
+
.check-info {
|
|
5796
|
+
display: inline-flex;
|
|
5797
|
+
align-items: center;
|
|
5798
|
+
justify-content: center;
|
|
5799
|
+
width: 1.125rem;
|
|
5800
|
+
height: 1.125rem;
|
|
5801
|
+
padding: 0;
|
|
5802
|
+
margin: 0;
|
|
5803
|
+
border: 1px solid var(--border);
|
|
5804
|
+
border-radius: 50%;
|
|
5805
|
+
background: #f1f5f9;
|
|
5806
|
+
color: var(--muted);
|
|
5807
|
+
font-size: 0.62rem;
|
|
5808
|
+
font-weight: 700;
|
|
5809
|
+
font-style: normal;
|
|
5810
|
+
line-height: 1;
|
|
5811
|
+
cursor: help;
|
|
5812
|
+
flex-shrink: 0;
|
|
5813
|
+
}
|
|
5814
|
+
.check-info:hover,
|
|
5815
|
+
.check-info:focus-visible {
|
|
5816
|
+
border-color: var(--accent);
|
|
5817
|
+
color: var(--accent);
|
|
5818
|
+
background: var(--accent-soft);
|
|
5819
|
+
outline: none;
|
|
5820
|
+
}
|
|
5821
|
+
.check-tooltip {
|
|
5822
|
+
position: absolute;
|
|
5823
|
+
left: 50%;
|
|
5824
|
+
bottom: calc(100% + 8px);
|
|
5825
|
+
transform: translateX(-50%);
|
|
5826
|
+
min-width: 12rem;
|
|
5827
|
+
max-width: min(22rem, 86vw);
|
|
5828
|
+
padding: 0.55rem 0.65rem;
|
|
5829
|
+
background: var(--text);
|
|
5830
|
+
color: #f8fafc;
|
|
5831
|
+
font-size: 0.78rem;
|
|
5832
|
+
font-weight: 400;
|
|
5833
|
+
line-height: 1.45;
|
|
5834
|
+
border-radius: 6px;
|
|
5835
|
+
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
|
|
5836
|
+
z-index: 50;
|
|
5837
|
+
opacity: 0;
|
|
5838
|
+
visibility: hidden;
|
|
5839
|
+
pointer-events: none;
|
|
5840
|
+
transition: opacity 0.12s ease, visibility 0.12s ease;
|
|
5841
|
+
text-align: left;
|
|
5842
|
+
}
|
|
5843
|
+
.check-info-wrap:hover .check-tooltip,
|
|
5844
|
+
.check-info-wrap:focus-within .check-tooltip {
|
|
5845
|
+
opacity: 1;
|
|
5846
|
+
visibility: visible;
|
|
5847
|
+
}
|
|
5848
|
+
.check-tooltip::after {
|
|
5849
|
+
content: '';
|
|
5850
|
+
position: absolute;
|
|
5851
|
+
top: 100%;
|
|
5852
|
+
left: 50%;
|
|
5853
|
+
margin-left: -6px;
|
|
5854
|
+
border: 6px solid transparent;
|
|
5855
|
+
border-top-color: var(--text);
|
|
5856
|
+
}
|
|
5561
5857
|
.dot {
|
|
5562
5858
|
display: inline-block;
|
|
5563
5859
|
width: 8px;
|