@cleartrip/frontguard 0.3.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -193
- package/dist/cli.js +600 -530
- package/dist/cli.js.map +1 -1
- package/package.json +11 -6
package/dist/cli.js
CHANGED
|
@@ -2402,199 +2402,209 @@ async function runMain(cmd, opts = {}) {
|
|
|
2402
2402
|
}
|
|
2403
2403
|
var CONFIG_TEMPLATE = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2404
2404
|
|
|
2405
|
+
/**
|
|
2406
|
+
* FrontGuard configuration.
|
|
2407
|
+
*
|
|
2408
|
+
* Severity levels: 'info' = note only | 'warn' = advisory | 'block' = fails CI in enforce mode
|
|
2409
|
+
* Gate modes: 'warn' (default) = advisory only | 'enforce' = exit 1 on any 'block' finding
|
|
2410
|
+
*
|
|
2411
|
+
* Run \`frontguard run\` locally to test your config before pushing.
|
|
2412
|
+
*/
|
|
2405
2413
|
export default defineConfig({
|
|
2406
|
-
//
|
|
2407
|
-
// 'warn' \u2014 findings are advisory; CI always passes (default).
|
|
2408
|
-
// 'enforce' \u2014 CI exits non-zero when any finding has severity 'block'.
|
|
2409
|
-
// Same as running \`frontguard run --enforce\`.
|
|
2414
|
+
// CI gate: 'warn' = never fails CI (default) | 'enforce' = fails CI on block-severity findings
|
|
2410
2415
|
mode: 'warn',
|
|
2411
2416
|
|
|
2412
|
-
//
|
|
2413
|
-
//
|
|
2414
|
-
// extends: '@your-org/frontguard-config/base',
|
|
2417
|
+
// Inherit defaults from a shared org config package (optional).
|
|
2418
|
+
// extends: '@your-org/frontguard-config',
|
|
2415
2419
|
|
|
2416
|
-
// \u2500\u2500
|
|
2417
|
-
//
|
|
2420
|
+
// \u2500\u2500 Custom rules \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2421
|
+
// Write your own checks as simple functions. Return true = rule violated.
|
|
2422
|
+
//
|
|
2418
2423
|
// rules: {
|
|
2419
|
-
// 'no-inline-style': {
|
|
2420
|
-
// severity: 'warn',
|
|
2421
|
-
// message: 'Avoid inline style objects in JSX',
|
|
2422
|
-
// check: (file) => /\\.tsx$/.test(file.path) && file.content.includes('style={{'),
|
|
2423
|
-
// },
|
|
2424
2424
|
// 'no-console-log': {
|
|
2425
|
-
// severity: '
|
|
2425
|
+
// severity: 'warn',
|
|
2426
2426
|
// message: 'Remove console.log before merging',
|
|
2427
2427
|
// check: (file) => /\\.(ts|tsx|js|jsx)$/.test(file.path) && /console\\.log\\(/.test(file.content),
|
|
2428
2428
|
// },
|
|
2429
|
+
// 'no-inline-styles': {
|
|
2430
|
+
// severity: 'info',
|
|
2431
|
+
// message: 'Prefer CSS classes over inline style objects',
|
|
2432
|
+
// check: (file) => /\\.tsx$/.test(file.path) && file.content.includes('style={{'),
|
|
2433
|
+
// },
|
|
2429
2434
|
// },
|
|
2430
2435
|
|
|
2431
2436
|
checks: {
|
|
2432
|
-
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
2437
|
+
|
|
2438
|
+
// \u2500\u2500 Linting (ESLint) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2439
|
+
// Uses your existing ESLint config. Runs on PR-changed files only when in CI.
|
|
2440
|
+
//
|
|
2435
2441
|
// eslint: {
|
|
2436
2442
|
// enabled: true,
|
|
2437
2443
|
// glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}',
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
// '**/frontguard.config.js',
|
|
2441
|
-
// '**/frontguard.config.cjs',
|
|
2442
|
-
// ],
|
|
2444
|
+
// // Files to skip (picomatch patterns). Defaults exclude frontguard.config.* files.
|
|
2445
|
+
// ignorePatterns: ['**/frontguard.config.mjs', '**/frontguard.config.js', '**/frontguard.config.cjs'],
|
|
2443
2446
|
// },
|
|
2444
2447
|
|
|
2445
|
-
// \u2500\u2500
|
|
2446
|
-
// Checks that files match Prettier
|
|
2447
|
-
//
|
|
2448
|
+
// \u2500\u2500 Formatting (Prettier) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2449
|
+
// Checks that files match your Prettier config.
|
|
2450
|
+
//
|
|
2448
2451
|
// prettier: {
|
|
2449
2452
|
// enabled: true,
|
|
2450
2453
|
// glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',
|
|
2451
|
-
// ignorePatterns: [
|
|
2452
|
-
// '**/frontguard.config.mjs',
|
|
2453
|
-
// '**/frontguard.config.js',
|
|
2454
|
-
// '**/frontguard.config.cjs',
|
|
2455
|
-
// ],
|
|
2454
|
+
// ignorePatterns: ['**/frontguard.config.mjs', '**/frontguard.config.js', '**/frontguard.config.cjs'],
|
|
2456
2455
|
// },
|
|
2457
2456
|
|
|
2458
|
-
// \u2500\u2500
|
|
2459
|
-
// Runs \`tsc --noEmit
|
|
2457
|
+
// \u2500\u2500 Type checking (TypeScript) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2458
|
+
// Runs \`tsc --noEmit\`. Requires a tsconfig.json in the project root.
|
|
2459
|
+
//
|
|
2460
2460
|
// typescript: {
|
|
2461
2461
|
// enabled: true,
|
|
2462
|
-
// tscArgs: [
|
|
2462
|
+
// tscArgs: [], // extra flags, e.g. ['--strict']
|
|
2463
2463
|
// },
|
|
2464
2464
|
|
|
2465
|
-
// \u2500\u2500
|
|
2466
|
-
// Scans
|
|
2465
|
+
// \u2500\u2500 Secret detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2466
|
+
// Scans for leaked API keys, tokens, and private keys using regex patterns.
|
|
2467
|
+
//
|
|
2467
2468
|
// secrets: { enabled: true },
|
|
2468
2469
|
|
|
2469
|
-
// \u2500\u2500
|
|
2470
|
-
//
|
|
2470
|
+
// \u2500\u2500 PR description quality \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2471
|
+
// Checks that the PR body meets your team's standards.
|
|
2472
|
+
//
|
|
2471
2473
|
// prHygiene: {
|
|
2472
2474
|
// enabled: true,
|
|
2473
|
-
// minBodyLength: 80,
|
|
2474
|
-
// requireSections: false,
|
|
2475
|
-
// sectionHints: ['what', 'why', 'test', 'screenshot'],
|
|
2476
|
-
// requireAiDisclosureSection: true,
|
|
2477
|
-
// gateWhenAiDisclosureAmbiguous: 'warn',
|
|
2475
|
+
// minBodyLength: 80, // minimum characters in PR description
|
|
2476
|
+
// requireSections: false, // set true to require section headers
|
|
2477
|
+
// sectionHints: ['what', 'why', 'test', 'screenshot'], // expected section keywords
|
|
2478
|
+
// requireAiDisclosureSection: true, // expect an ## AI disclosure section
|
|
2479
|
+
// gateWhenAiDisclosureAmbiguous: 'warn', // 'info' | 'warn' | 'block'
|
|
2478
2480
|
// },
|
|
2479
2481
|
|
|
2480
|
-
// \u2500\u2500
|
|
2481
|
-
// Flags
|
|
2482
|
+
// \u2500\u2500 PR size limits \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2483
|
+
// Flags PRs that are too large to review effectively.
|
|
2484
|
+
//
|
|
2482
2485
|
// prSize: {
|
|
2483
2486
|
// enabled: true,
|
|
2484
|
-
//
|
|
2485
|
-
//
|
|
2486
|
-
// //
|
|
2487
|
+
// // Simple mode: two thresholds
|
|
2488
|
+
// warnLines: 400, // warn above this many changed lines
|
|
2489
|
+
// softBlockLines: 800, // block above this many changed lines
|
|
2490
|
+
// // Advanced mode: custom tiers (overrides warnLines/softBlockLines when set)
|
|
2487
2491
|
// // tiers: [
|
|
2488
|
-
// // { minLines: 1000, severity: 'block', message: 'PR too large (\${lines} lines)' },
|
|
2489
|
-
// // { minLines: 500, severity: 'warn', message: '
|
|
2492
|
+
// // { minLines: 1000, severity: 'block', message: 'PR too large (\${lines} lines). Split it up.' },
|
|
2493
|
+
// // { minLines: 500, severity: 'warn', message: 'Large PR (\${lines} lines). Consider splitting.' },
|
|
2490
2494
|
// // ],
|
|
2491
2495
|
// },
|
|
2492
2496
|
|
|
2493
|
-
// \u2500\u2500
|
|
2494
|
-
// Counts new \`any\`
|
|
2497
|
+
// \u2500\u2500 TypeScript \`any\` usage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2498
|
+
// Counts new \`any\` types introduced in this diff. Helps prevent type safety erosion.
|
|
2499
|
+
//
|
|
2495
2500
|
// tsAnyDelta: {
|
|
2496
2501
|
// enabled: true,
|
|
2497
|
-
// gate: 'warn',
|
|
2498
|
-
// baseRef: 'main',
|
|
2499
|
-
// maxAdded: 0,
|
|
2502
|
+
// gate: 'warn', // severity when maxAdded is exceeded: 'info' | 'warn' | 'block'
|
|
2503
|
+
// baseRef: 'main', // branch to compare against (overridden by BITBUCKET_PR_DESTINATION_BRANCH)
|
|
2504
|
+
// maxAdded: 0, // 0 = report the count; >0 = trigger gate only when exceeded
|
|
2500
2505
|
// },
|
|
2501
2506
|
|
|
2502
|
-
// \u2500\u2500
|
|
2503
|
-
//
|
|
2507
|
+
// \u2500\u2500 Circular import detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2508
|
+
// Uses \`madge\` to find circular dependencies in your source tree.
|
|
2509
|
+
//
|
|
2504
2510
|
// cycles: {
|
|
2505
2511
|
// enabled: true,
|
|
2506
2512
|
// gate: 'warn',
|
|
2507
|
-
// entries: ['src'],
|
|
2508
|
-
// extraArgs: [],
|
|
2513
|
+
// entries: ['src'], // directories to analyse
|
|
2514
|
+
// extraArgs: [], // extra flags forwarded to madge
|
|
2509
2515
|
// },
|
|
2510
2516
|
|
|
2511
|
-
// \u2500\u2500
|
|
2512
|
-
//
|
|
2517
|
+
// \u2500\u2500 Unused exports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2518
|
+
// Uses \`ts-prune\` to surface exports that are never imported.
|
|
2519
|
+
//
|
|
2513
2520
|
// deadCode: {
|
|
2514
2521
|
// enabled: true,
|
|
2515
|
-
// gate: 'info',
|
|
2516
|
-
//
|
|
2517
|
-
// maxReportLines: 80,
|
|
2522
|
+
// gate: 'info', // usually kept as info to avoid noise
|
|
2523
|
+
// maxReportLines: 80, // cap output length
|
|
2518
2524
|
// },
|
|
2519
2525
|
|
|
2520
|
-
// \u2500\u2500
|
|
2521
|
-
//
|
|
2522
|
-
// reactNative: {
|
|
2523
|
-
// enabled: true,
|
|
2524
|
-
// gate: 'info',
|
|
2525
|
-
// requireMetroConfig: true,
|
|
2526
|
-
// runAlignDeps: false, // npx @rnx-kit/align-deps (registry access)
|
|
2527
|
-
// alignDepsArgs: ['--requirements', 'react-native'],
|
|
2528
|
-
// runDoctor: false, // local react-native doctor (slow / env-specific)
|
|
2529
|
-
// swiftLintOnChangedSwift: true,
|
|
2530
|
-
// hintAndroidNativeOnPr: true,
|
|
2531
|
-
// },
|
|
2532
|
-
|
|
2533
|
-
// \u2500\u2500\u2500 Bundle size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2534
|
-
// Measures bundle after build and compares to a baseline.
|
|
2526
|
+
// \u2500\u2500 Bundle size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2527
|
+
// Builds your project and measures the output size against a committed baseline.
|
|
2535
2528
|
//
|
|
2536
|
-
//
|
|
2537
|
-
// 'auto' \u2014 auto-
|
|
2538
|
-
// 'next' \u2014
|
|
2539
|
-
// 'vite' \u2014
|
|
2540
|
-
// 'cra' \u2014
|
|
2541
|
-
// 'glob' \u2014
|
|
2542
|
-
// 'custom' \u2014
|
|
2529
|
+
// Strategy options:
|
|
2530
|
+
// 'auto' \u2014 auto-detects from package.json (Next.js \u2192 next, Vite \u2192 vite, CRA \u2192 cra, else \u2192 glob)
|
|
2531
|
+
// 'next' \u2014 reads "First Load JS shared by all" from next build output
|
|
2532
|
+
// 'vite' \u2014 sums dist/assets/*.js
|
|
2533
|
+
// 'cra' \u2014 reads gzipped JS from react-scripts build output
|
|
2534
|
+
// 'glob' \u2014 sums all files matching measureGlobs
|
|
2535
|
+
// 'custom' \u2014 runs bundleSizeCommand and reads a number (bytes) from stdout
|
|
2543
2536
|
//
|
|
2544
2537
|
// bundle: {
|
|
2545
2538
|
// enabled: true,
|
|
2546
2539
|
// gate: 'warn',
|
|
2547
|
-
//
|
|
2548
|
-
//
|
|
2549
|
-
//
|
|
2550
|
-
// bundleSizeCommand: null, // only for strategy 'custom', e.g. 'node scripts/bundle-size.js'
|
|
2540
|
+
// buildCommand: 'npm run build', // command to build your project
|
|
2541
|
+
// bundleSizeStrategy: 'auto', // see above
|
|
2542
|
+
// bundleSizeCommand: null, // only needed when strategy is 'custom'
|
|
2551
2543
|
// measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],
|
|
2552
|
-
// baselinePath: '.frontguard/bundle-baseline.json',
|
|
2553
|
-
// baselineRef: 'main',
|
|
2554
|
-
// maxDeltaBytes: 50_000,
|
|
2555
|
-
// maxTotalBytes: null,
|
|
2544
|
+
// baselinePath: '.frontguard/bundle-baseline.json', // committed baseline file
|
|
2545
|
+
// baselineRef: 'main', // git ref to read baseline from if file missing
|
|
2546
|
+
// maxDeltaBytes: 50_000, // max allowed growth vs baseline (null = no limit)
|
|
2547
|
+
// maxTotalBytes: null, // absolute size cap (null = no limit)
|
|
2548
|
+
// runBuild: true, // set false to skip build and measure existing files
|
|
2556
2549
|
// },
|
|
2557
2550
|
|
|
2558
|
-
// \u2500\u2500
|
|
2559
|
-
// Static
|
|
2551
|
+
// \u2500\u2500 Core Web Vitals hints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2552
|
+
// Static analysis of JSX/TSX for patterns that harm LCP, CLS, or FID.
|
|
2553
|
+
// Not a substitute for real field data (Lighthouse, CrUX).
|
|
2554
|
+
//
|
|
2560
2555
|
// coreWebVitals: {
|
|
2561
2556
|
// enabled: true,
|
|
2562
2557
|
// gate: 'warn',
|
|
2563
2558
|
// scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],
|
|
2564
|
-
// maxFileBytes: 400_000,
|
|
2565
2559
|
// },
|
|
2566
2560
|
|
|
2567
|
-
// \u2500\u2500
|
|
2568
|
-
//
|
|
2569
|
-
//
|
|
2561
|
+
// \u2500\u2500 React Native hygiene \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2562
|
+
// Runs only when react-native is in your dependencies. Checks Metro config,
|
|
2563
|
+
// optionally runs align-deps and react-native doctor, and hints on native file changes.
|
|
2564
|
+
//
|
|
2565
|
+
// reactNative: {
|
|
2570
2566
|
// enabled: true,
|
|
2567
|
+
// gate: 'info',
|
|
2568
|
+
// requireMetroConfig: true, // warn when metro.config.* is missing
|
|
2569
|
+
// runAlignDeps: false, // run @rnx-kit/align-deps (needs network/registry)
|
|
2570
|
+
// alignDepsArgs: ['--requirements', 'react-native'],
|
|
2571
|
+
// runDoctor: false, // run react-native doctor (slow, macOS-heavy)
|
|
2572
|
+
// swiftLintOnChangedSwift: true, // run SwiftLint when PR touches .swift files
|
|
2573
|
+
// hintAndroidNativeOnPr: true, // remind to run Android Lint when .kt/.java change
|
|
2574
|
+
// },
|
|
2575
|
+
|
|
2576
|
+
// \u2500\u2500 AI-assisted strict checks (off by default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2577
|
+
// When enabled: scans code marked with @frontguard-ai comments (or AI-disclosed PRs)
|
|
2578
|
+
// for risky patterns \u2014 eval(), dangerouslySetInnerHTML, @ts-ignore, empty catch, etc.
|
|
2579
|
+
//
|
|
2580
|
+
// aiAssistedReview: {
|
|
2581
|
+
// enabled: false,
|
|
2571
2582
|
// gate: 'warn',
|
|
2572
|
-
// // 'decorator'
|
|
2573
|
-
// // 'pr-disclosure'
|
|
2574
|
-
// // 'both'
|
|
2583
|
+
// // 'decorator' \u2014 only scan regions marked with @frontguard-ai:start / :end
|
|
2584
|
+
// // 'pr-disclosure' \u2014 scan all touched files when PR discloses AI use
|
|
2585
|
+
// // 'both' \u2014 decorator regions when present; PR disclosure as fallback
|
|
2575
2586
|
// strictScanMode: 'both',
|
|
2576
2587
|
// escalate: {
|
|
2577
|
-
// secretFindingsToBlock: true,
|
|
2578
|
-
// tsAnyDeltaToBlock: true,
|
|
2588
|
+
// secretFindingsToBlock: true, // promote secret findings to 'block' in AI PRs
|
|
2589
|
+
// tsAnyDeltaToBlock: true, // promote any-delta findings to 'block' in AI PRs
|
|
2579
2590
|
// },
|
|
2580
2591
|
// },
|
|
2581
2592
|
|
|
2582
|
-
// \u2500\u2500
|
|
2583
|
-
//
|
|
2584
|
-
//
|
|
2585
|
-
//
|
|
2593
|
+
// \u2500\u2500 LLM-assisted review (off by default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2594
|
+
// Attaches an automated diff review from an LLM to the report.
|
|
2595
|
+
// Supports OpenAI, Anthropic, or a local Ollama instance.
|
|
2596
|
+
//
|
|
2586
2597
|
// llm: {
|
|
2587
|
-
// enabled:
|
|
2588
|
-
// provider: 'ollama',
|
|
2589
|
-
// model: 'llama3.2',
|
|
2590
|
-
//
|
|
2591
|
-
//
|
|
2598
|
+
// enabled: false,
|
|
2599
|
+
// provider: 'ollama', // 'openai' | 'anthropic' | 'ollama'
|
|
2600
|
+
// model: 'llama3.2', // model name
|
|
2601
|
+
// apiKeyEnv: 'OPENAI_API_KEY', // env var holding the API key (cloud providers only)
|
|
2602
|
+
// ollamaUrl: 'http://127.0.0.1:11434', // Ollama base URL
|
|
2592
2603
|
// maxDiffChars: 48_000,
|
|
2593
2604
|
// timeoutMs: 60_000,
|
|
2594
|
-
// perFindingFixes: false,
|
|
2595
|
-
// maxFixSuggestions: 12,
|
|
2596
|
-
// maxFileContextChars: 24_000,
|
|
2605
|
+
// perFindingFixes: false, // ask for fix suggestions per finding (slower)
|
|
2597
2606
|
// },
|
|
2607
|
+
|
|
2598
2608
|
},
|
|
2599
2609
|
})
|
|
2600
2610
|
`;
|
|
@@ -2865,8 +2875,8 @@ function mdTableCell(s3) {
|
|
|
2865
2875
|
function statusForPrTable(r4) {
|
|
2866
2876
|
const n3 = r4.findings.length;
|
|
2867
2877
|
const blocks = r4.findings.filter((f4) => f4.severity === "block").length;
|
|
2868
|
-
if (blocks > 0) return `${n3} issue
|
|
2869
|
-
return `${n3} issue
|
|
2878
|
+
if (blocks > 0) return `${n3} issue${n3 === 1 ? "" : "s"}, ${blocks} blocking`;
|
|
2879
|
+
return `${n3} issue${n3 === 1 ? "" : "s"}`;
|
|
2870
2880
|
}
|
|
2871
2881
|
function checkNeedsRow(r4) {
|
|
2872
2882
|
return !r4.skipped && r4.findings.length > 0;
|
|
@@ -2882,21 +2892,21 @@ function formatBitbucketPrSnippet(report) {
|
|
|
2882
2892
|
const reportUrl = publicReport || FRONTGUARD_REPORT_URL_PLACEHOLDER;
|
|
2883
2893
|
const { riskScore, results } = report;
|
|
2884
2894
|
const lines = [];
|
|
2885
|
-
|
|
2895
|
+
const riskEmoji2 = riskScore === "HIGH" ? "\u{1F534}" : riskScore === "MEDIUM" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2896
|
+
lines.push(`**FrontGuard** \xB7 ${riskEmoji2} ${riskScore} risk`);
|
|
2886
2897
|
lines.push("");
|
|
2887
2898
|
const failing = results.filter(checkNeedsRow);
|
|
2888
2899
|
if (failing.length > 0) {
|
|
2889
|
-
lines.push("| Check
|
|
2890
|
-
lines.push("|
|
|
2900
|
+
lines.push("| Check | Issues |");
|
|
2901
|
+
lines.push("| :-- | :-- |");
|
|
2891
2902
|
for (const r4 of failing) {
|
|
2892
|
-
lines.push(`|
|
|
2903
|
+
lines.push(`| \`${mdTableCell(r4.checkId)}\` | ${mdTableCell(statusForPrTable(r4))} |`);
|
|
2893
2904
|
}
|
|
2894
2905
|
} else {
|
|
2895
|
-
lines.push("_All checks passed
|
|
2906
|
+
lines.push("_All checks passed._");
|
|
2896
2907
|
}
|
|
2897
2908
|
lines.push("");
|
|
2898
2909
|
lines.push(DETAILED_REPORT_LINE);
|
|
2899
|
-
lines.push("");
|
|
2900
2910
|
lines.push(reportUrl.endsWith("\n") ? reportUrl.slice(0, -1) : reportUrl);
|
|
2901
2911
|
lines.push("");
|
|
2902
2912
|
return lines.join("\n");
|
|
@@ -6371,438 +6381,519 @@ function sortFindings(cwd, items) {
|
|
|
6371
6381
|
});
|
|
6372
6382
|
}
|
|
6373
6383
|
function formatDuration(ms) {
|
|
6374
|
-
if (ms < 1e3) return `${ms}
|
|
6384
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
6375
6385
|
const s3 = Math.round(ms / 1e3);
|
|
6376
6386
|
if (s3 < 60) return `${s3}s`;
|
|
6377
6387
|
const m3 = Math.floor(s3 / 60);
|
|
6378
6388
|
const r4 = s3 % 60;
|
|
6379
6389
|
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
6380
6390
|
}
|
|
6381
|
-
function
|
|
6382
|
-
if (r4.skipped) return '<span class="
|
|
6383
|
-
if (r4.findings.length === 0) return '<span class="
|
|
6391
|
+
function checkStatusIcon(r4) {
|
|
6392
|
+
if (r4.skipped) return '<span class="status-icon skip" title="Skipped">\u2013</span>';
|
|
6393
|
+
if (r4.findings.length === 0) return '<span class="status-icon pass" title="Passed">\u2713</span>';
|
|
6384
6394
|
if (r4.findings.some((x3) => x3.severity === "block"))
|
|
6385
|
-
return '<span class="
|
|
6386
|
-
return '<span class="
|
|
6387
|
-
}
|
|
6388
|
-
var CHECKS_TABLE_STYLES = `
|
|
6389
|
-
table.results {
|
|
6390
|
-
width: 100%;
|
|
6391
|
-
border-collapse: collapse;
|
|
6392
|
-
font-size: 0.875rem;
|
|
6393
|
-
background: var(--surface);
|
|
6394
|
-
border-radius: var(--radius);
|
|
6395
|
-
overflow: hidden;
|
|
6396
|
-
border: 1px solid var(--border);
|
|
6397
|
-
box-shadow: var(--shadow);
|
|
6398
|
-
}
|
|
6399
|
-
table.results th, table.results td {
|
|
6400
|
-
padding: 0.55rem 0.85rem;
|
|
6401
|
-
text-align: left;
|
|
6402
|
-
border-bottom: 1px solid var(--border);
|
|
6403
|
-
}
|
|
6404
|
-
table.results tr:last-child td { border-bottom: none; }
|
|
6405
|
-
table.results thead th {
|
|
6406
|
-
background: #f1f5f9;
|
|
6407
|
-
color: var(--muted);
|
|
6408
|
-
font-weight: 600;
|
|
6409
|
-
font-size: 0.72rem;
|
|
6410
|
-
text-transform: uppercase;
|
|
6411
|
-
letter-spacing: 0.04em;
|
|
6412
|
-
}
|
|
6413
|
-
.td-icon { width: 2rem; vertical-align: middle; }
|
|
6414
|
-
.td-check { vertical-align: middle; }
|
|
6415
|
-
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
6416
|
-
.check-title-cell {
|
|
6417
|
-
display: inline-flex;
|
|
6418
|
-
align-items: center;
|
|
6419
|
-
gap: 0.35rem;
|
|
6420
|
-
flex-wrap: nowrap;
|
|
6421
|
-
}
|
|
6422
|
-
.check-name { font-weight: 600; }
|
|
6423
|
-
.check-info-wrap {
|
|
6424
|
-
position: relative;
|
|
6425
|
-
display: inline-flex;
|
|
6426
|
-
align-items: center;
|
|
6427
|
-
flex-shrink: 0;
|
|
6428
|
-
}
|
|
6429
|
-
.check-info {
|
|
6430
|
-
display: inline-flex;
|
|
6431
|
-
align-items: center;
|
|
6432
|
-
justify-content: center;
|
|
6433
|
-
width: 1.125rem;
|
|
6434
|
-
height: 1.125rem;
|
|
6435
|
-
padding: 0;
|
|
6436
|
-
margin: 0;
|
|
6437
|
-
border: 1px solid var(--border);
|
|
6438
|
-
border-radius: 50%;
|
|
6439
|
-
background: #f1f5f9;
|
|
6440
|
-
color: var(--muted);
|
|
6441
|
-
font-size: 0.62rem;
|
|
6442
|
-
font-weight: 700;
|
|
6443
|
-
font-style: normal;
|
|
6444
|
-
line-height: 1;
|
|
6445
|
-
cursor: help;
|
|
6446
|
-
flex-shrink: 0;
|
|
6447
|
-
}
|
|
6448
|
-
.check-info:hover,
|
|
6449
|
-
.check-info:focus-visible {
|
|
6450
|
-
border-color: var(--accent);
|
|
6451
|
-
color: var(--accent);
|
|
6452
|
-
background: var(--accent-soft);
|
|
6453
|
-
outline: none;
|
|
6454
|
-
}
|
|
6455
|
-
.check-tooltip {
|
|
6456
|
-
position: absolute;
|
|
6457
|
-
left: 50%;
|
|
6458
|
-
bottom: calc(100% + 8px);
|
|
6459
|
-
transform: translateX(-50%);
|
|
6460
|
-
min-width: 12rem;
|
|
6461
|
-
max-width: min(22rem, 86vw);
|
|
6462
|
-
padding: 0.55rem 0.65rem;
|
|
6463
|
-
background: var(--text);
|
|
6464
|
-
color: #f8fafc;
|
|
6465
|
-
font-size: 0.78rem;
|
|
6466
|
-
font-weight: 400;
|
|
6467
|
-
line-height: 1.45;
|
|
6468
|
-
border-radius: 6px;
|
|
6469
|
-
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
|
|
6470
|
-
z-index: 50;
|
|
6471
|
-
opacity: 0;
|
|
6472
|
-
visibility: hidden;
|
|
6473
|
-
pointer-events: none;
|
|
6474
|
-
transition: opacity 0.12s ease, visibility 0.12s ease;
|
|
6475
|
-
text-align: left;
|
|
6476
|
-
}
|
|
6477
|
-
.check-info-wrap:hover .check-tooltip,
|
|
6478
|
-
.check-info-wrap:focus-within .check-tooltip {
|
|
6479
|
-
opacity: 1;
|
|
6480
|
-
visibility: visible;
|
|
6481
|
-
}
|
|
6482
|
-
.check-tooltip::after {
|
|
6483
|
-
content: '';
|
|
6484
|
-
position: absolute;
|
|
6485
|
-
top: 100%;
|
|
6486
|
-
left: 50%;
|
|
6487
|
-
margin-left: -6px;
|
|
6488
|
-
border: 6px solid transparent;
|
|
6489
|
-
border-top-color: var(--text);
|
|
6490
|
-
}
|
|
6491
|
-
.dot {
|
|
6492
|
-
display: inline-block;
|
|
6493
|
-
width: 8px;
|
|
6494
|
-
height: 8px;
|
|
6495
|
-
border-radius: 50%;
|
|
6496
|
-
}
|
|
6497
|
-
.dot-ok { background: var(--ok); }
|
|
6498
|
-
.dot-warn { background: var(--warn); }
|
|
6499
|
-
.dot-block { background: var(--block); }
|
|
6500
|
-
.dot-skip { background: #cbd5e1; }
|
|
6501
|
-
`;
|
|
6502
|
-
function renderCheckTableRows(results) {
|
|
6503
|
-
return results.map((r4) => {
|
|
6504
|
-
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
6505
|
-
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6506
|
-
const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
|
|
6507
|
-
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>`;
|
|
6508
|
-
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>`;
|
|
6509
|
-
}).join("\n");
|
|
6395
|
+
return '<span class="status-icon block" title="Blocking">\u2715</span>';
|
|
6396
|
+
return '<span class="status-icon warn" title="Warnings">!</span>';
|
|
6510
6397
|
}
|
|
6511
6398
|
function renderFindingCard(cwd, r4, f4) {
|
|
6512
6399
|
const d3 = normalizeFinding(cwd, f4);
|
|
6513
6400
|
const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
|
|
6514
|
-
const
|
|
6515
|
-
const
|
|
6516
|
-
|
|
6517
|
-
|
|
6401
|
+
const sevLabel = f4.severity === "block" ? "blocking" : f4.severity === "warn" ? "warning" : "info";
|
|
6402
|
+
const meta = [];
|
|
6403
|
+
meta.push(`<span class="tag tag-check">${escapeHtml(r4.checkId)}</span>`);
|
|
6404
|
+
meta.push(`<span class="tag tag-rule">${escapeHtml(f4.id)}</span>`);
|
|
6405
|
+
if (d3.file) meta.push(`<span class="tag tag-file">${escapeHtml(d3.file)}</span>`);
|
|
6406
|
+
const hintHtml = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 240 && !d3.detail.includes("|") ? `<p class="card-hint">${escapeHtml(d3.detail)}</p>` : "";
|
|
6407
|
+
const detailHtml = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 240 || d3.detail.includes("|")) ? `<pre class="code-block"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
|
|
6408
|
+
const fixHtml = f4.suggestedFix ? `<div class="fix-box"><div class="fix-header">Suggested fix <span class="badge badge-llm">LLM</span></div><p class="fix-summary">${escapeHtml(f4.suggestedFix.summary)}</p>${f4.suggestedFix.code ? `<pre class="code-block"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="fix-disclaimer">Non-binding \u2014 review before applying.</p></div>` : "";
|
|
6409
|
+
return `<div class="card ${sevClass}"><div class="card-header"><span class="sev-badge ${sevClass}">${sevLabel}</span><p class="card-message">${escapeHtml(d3.message)}</p></div><div class="card-meta">${meta.join("")}</div>${hintHtml}${detailHtml}${fixHtml}</div>`;
|
|
6410
|
+
}
|
|
6411
|
+
function renderCheckTableRows(_cwd, results) {
|
|
6412
|
+
return results.map((r4) => {
|
|
6413
|
+
const icon = checkStatusIcon(r4);
|
|
6414
|
+
const statusText = r4.skipped ? `<span class="status-text muted">Skipped \u2014 ${escapeHtml(r4.skipped)}</span>` : r4.findings.length === 0 ? '<span class="status-text ok">Passed</span>' : r4.findings.some((f4) => f4.severity === "block") ? `<span class="status-text danger">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>` : `<span class="status-text caution">${r4.findings.length} issue${r4.findings.length === 1 ? "" : "s"}</span>`;
|
|
6415
|
+
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
6416
|
+
const countCell = r4.skipped ? "\u2014" : String(r4.findings.length);
|
|
6417
|
+
const timeCell = formatDuration(r4.durationMs);
|
|
6418
|
+
return `<tr><td class="col-icon">${icon}</td><td class="col-check"><span class="check-name">${escapeHtml(r4.checkId)}</span><span class="check-tooltip">${help}</span></td><td class="col-status">${statusText}</td><td class="col-count">${countCell}</td><td class="col-time">${timeCell}</td></tr>`;
|
|
6419
|
+
}).join("\n");
|
|
6518
6420
|
}
|
|
6519
6421
|
function buildHtmlReport(p2) {
|
|
6520
|
-
const {
|
|
6521
|
-
cwd,
|
|
6522
|
-
riskScore,
|
|
6523
|
-
mode,
|
|
6524
|
-
stack,
|
|
6525
|
-
pr,
|
|
6526
|
-
results,
|
|
6527
|
-
warns,
|
|
6528
|
-
infos,
|
|
6529
|
-
blocks,
|
|
6530
|
-
lines,
|
|
6531
|
-
llmAppendix
|
|
6532
|
-
} = p2;
|
|
6533
|
-
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6422
|
+
const { cwd, riskScore, mode, stack, pr, results, warns, infos, blocks, lines, llmAppendix } = p2;
|
|
6534
6423
|
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
6535
|
-
const
|
|
6536
|
-
const
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
);
|
|
6542
|
-
const warnItems = sortFindings(
|
|
6543
|
-
cwd,
|
|
6544
|
-
results.flatMap(
|
|
6545
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
6546
|
-
)
|
|
6547
|
-
);
|
|
6548
|
-
const infoItems = sortFindings(
|
|
6549
|
-
cwd,
|
|
6550
|
-
results.flatMap(
|
|
6551
|
-
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
6552
|
-
)
|
|
6553
|
-
);
|
|
6424
|
+
const riskIcon = riskScore === "LOW" ? "\u2713" : riskScore === "MEDIUM" ? "!" : "\u2715";
|
|
6425
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
6426
|
+
const checkRows = renderCheckTableRows(cwd, results);
|
|
6427
|
+
const blockItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))));
|
|
6428
|
+
const warnItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))));
|
|
6429
|
+
const infoItems = sortFindings(cwd, results.flatMap((r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))));
|
|
6554
6430
|
const byCheck = /* @__PURE__ */ new Map();
|
|
6555
6431
|
for (const item of warnItems) {
|
|
6556
6432
|
const list = byCheck.get(item.r.checkId) ?? [];
|
|
6557
6433
|
list.push(item);
|
|
6558
6434
|
byCheck.set(item.r.checkId, list);
|
|
6559
6435
|
}
|
|
6560
|
-
const
|
|
6561
|
-
const
|
|
6562
|
-
let
|
|
6436
|
+
const warnCheckOrder = [...byCheck.keys()].sort();
|
|
6437
|
+
const blockHtml = blockItems.length === 0 ? '<p class="empty-note">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
6438
|
+
let warnsHtml = "";
|
|
6563
6439
|
if (warnItems.length === 0) {
|
|
6564
|
-
|
|
6440
|
+
warnsHtml = '<p class="empty-note">No warnings.</p>';
|
|
6565
6441
|
} else {
|
|
6566
|
-
for (const cid of
|
|
6442
|
+
for (const cid of warnCheckOrder) {
|
|
6567
6443
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
6568
|
-
|
|
6569
|
-
warningsHtml += `<details class="panel nested"><summary><span class="summary-title">${escapeHtml(cid)}</span><span class="summary-count">${group.length}</span></summary><div class="panel-body">${cards}</div></details>`;
|
|
6444
|
+
warnsHtml += `<div class="group"><div class="group-label"><span class="group-name">${escapeHtml(cid)}</span><span class="group-count">${group.length}</span></div>${group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n")}</div>`;
|
|
6570
6445
|
}
|
|
6571
6446
|
}
|
|
6572
|
-
const infoHtml = infoItems.length === 0 ? '<p class="empty-
|
|
6573
|
-
const
|
|
6574
|
-
const
|
|
6447
|
+
const infoHtml = infoItems.length === 0 ? '<p class="empty-note muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
6448
|
+
const prRow = pr && lines != null ? `<div class="meta-row"><span class="meta-label">PR size</span><span class="meta-value">${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</span></div>` : "";
|
|
6449
|
+
const appendixHtml = llmAppendix?.trim() ? `<section class="section"><h2 class="section-title">Appendix</h2><pre class="raw-pre">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
|
|
6450
|
+
const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
|
6575
6451
|
return `<!DOCTYPE html>
|
|
6576
6452
|
<html lang="en">
|
|
6577
6453
|
<head>
|
|
6578
6454
|
<meta charset="utf-8" />
|
|
6579
6455
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6580
|
-
<title>FrontGuard
|
|
6456
|
+
<title>FrontGuard Report</title>
|
|
6581
6457
|
<style>
|
|
6582
6458
|
:root {
|
|
6583
6459
|
--bg: #f8fafc;
|
|
6584
6460
|
--surface: #ffffff;
|
|
6585
|
-
--
|
|
6586
|
-
--muted: #64748b;
|
|
6461
|
+
--surface-alt: #f1f5f9;
|
|
6587
6462
|
--border: #e2e8f0;
|
|
6588
|
-
--
|
|
6589
|
-
--
|
|
6590
|
-
--
|
|
6591
|
-
--
|
|
6592
|
-
--
|
|
6593
|
-
--
|
|
6463
|
+
--border-light: #f1f5f9;
|
|
6464
|
+
--text: #0f172a;
|
|
6465
|
+
--text-secondary: #475569;
|
|
6466
|
+
--muted: #94a3b8;
|
|
6467
|
+
--accent: #6366f1;
|
|
6468
|
+
--c-block: #dc2626;
|
|
6469
|
+
--c-block-bg: #fef2f2;
|
|
6470
|
+
--c-block-border: #fecaca;
|
|
6471
|
+
--c-warn: #d97706;
|
|
6472
|
+
--c-warn-bg: #fffbeb;
|
|
6473
|
+
--c-warn-border: #fde68a;
|
|
6474
|
+
--c-info: #2563eb;
|
|
6475
|
+
--c-info-bg: #eff6ff;
|
|
6476
|
+
--c-info-border: #bfdbfe;
|
|
6477
|
+
--c-ok: #16a34a;
|
|
6478
|
+
--c-ok-bg: #f0fdf4;
|
|
6479
|
+
--radius-sm: 6px;
|
|
6594
6480
|
--radius: 10px;
|
|
6595
|
-
--
|
|
6481
|
+
--radius-lg: 14px;
|
|
6482
|
+
--shadow-sm: 0 1px 2px rgba(0,0,0,.05);
|
|
6483
|
+
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
|
|
6484
|
+
font-size: 15px;
|
|
6596
6485
|
}
|
|
6597
|
-
|
|
6486
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
6598
6487
|
body {
|
|
6599
|
-
|
|
6600
|
-
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
6488
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
6601
6489
|
background: var(--bg);
|
|
6602
6490
|
color: var(--text);
|
|
6603
|
-
line-height: 1.
|
|
6604
|
-
|
|
6605
|
-
padding: 2rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
6606
|
-
max-width: 920px;
|
|
6607
|
-
margin-left: auto;
|
|
6608
|
-
margin-right: auto;
|
|
6609
|
-
}
|
|
6610
|
-
.hero {
|
|
6611
|
-
margin-bottom: 2rem;
|
|
6491
|
+
line-height: 1.6;
|
|
6492
|
+
padding: 2.5rem clamp(1rem, 5vw, 3rem) 5rem;
|
|
6612
6493
|
}
|
|
6494
|
+
.wrap { max-width: 860px; margin: 0 auto; }
|
|
6495
|
+
|
|
6496
|
+
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6497
|
+
.header { margin-bottom: 2.5rem; }
|
|
6613
6498
|
.brand {
|
|
6614
|
-
font-size: 0.
|
|
6615
|
-
font-weight:
|
|
6499
|
+
font-size: 0.7rem;
|
|
6500
|
+
font-weight: 700;
|
|
6616
6501
|
letter-spacing: 0.12em;
|
|
6617
6502
|
text-transform: uppercase;
|
|
6618
6503
|
color: var(--muted);
|
|
6619
|
-
margin-bottom: 0.
|
|
6504
|
+
margin-bottom: 0.75rem;
|
|
6620
6505
|
}
|
|
6621
|
-
|
|
6622
|
-
font-size: 1.
|
|
6506
|
+
.page-title {
|
|
6507
|
+
font-size: 1.6rem;
|
|
6623
6508
|
font-weight: 700;
|
|
6624
6509
|
letter-spacing: -0.03em;
|
|
6625
|
-
margin: 0 0 1rem;
|
|
6626
6510
|
color: var(--text);
|
|
6511
|
+
margin-bottom: 1.25rem;
|
|
6512
|
+
line-height: 1.2;
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
/* \u2500\u2500 Risk pill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6516
|
+
.risk-pill {
|
|
6517
|
+
display: inline-flex;
|
|
6518
|
+
align-items: center;
|
|
6519
|
+
gap: 0.45rem;
|
|
6520
|
+
padding: 0.35rem 0.8rem;
|
|
6521
|
+
border-radius: 999px;
|
|
6522
|
+
font-size: 0.82rem;
|
|
6523
|
+
font-weight: 700;
|
|
6524
|
+
letter-spacing: 0.02em;
|
|
6525
|
+
border: 1.5px solid;
|
|
6627
6526
|
}
|
|
6628
|
-
.
|
|
6527
|
+
.risk-low { color: var(--c-ok); background: var(--c-ok-bg); border-color: #bbf7d0; }
|
|
6528
|
+
.risk-med { color: var(--c-warn); background: var(--c-warn-bg); border-color: var(--c-warn-border); }
|
|
6529
|
+
.risk-high { color: var(--c-block); background: var(--c-block-bg); border-color: var(--c-block-border); }
|
|
6530
|
+
.risk-icon { font-size: 0.75rem; }
|
|
6531
|
+
|
|
6532
|
+
/* \u2500\u2500 Stat chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6533
|
+
.stats {
|
|
6629
6534
|
display: flex;
|
|
6630
6535
|
flex-wrap: wrap;
|
|
6631
|
-
gap: 0.
|
|
6632
|
-
margin-
|
|
6536
|
+
gap: 0.5rem;
|
|
6537
|
+
margin-top: 1rem;
|
|
6538
|
+
align-items: center;
|
|
6633
6539
|
}
|
|
6634
|
-
.
|
|
6540
|
+
.stat {
|
|
6541
|
+
display: flex;
|
|
6542
|
+
align-items: center;
|
|
6543
|
+
gap: 0.35rem;
|
|
6544
|
+
padding: 0.3rem 0.65rem;
|
|
6545
|
+
background: var(--surface);
|
|
6546
|
+
border: 1px solid var(--border);
|
|
6547
|
+
border-radius: var(--radius-sm);
|
|
6548
|
+
font-size: 0.78rem;
|
|
6549
|
+
box-shadow: var(--shadow-sm);
|
|
6550
|
+
}
|
|
6551
|
+
.stat-label { color: var(--text-secondary); }
|
|
6552
|
+
.stat-value { font-weight: 600; color: var(--text); }
|
|
6553
|
+
.stat-value.danger { color: var(--c-block); }
|
|
6554
|
+
.stat-value.caution { color: var(--c-warn); }
|
|
6555
|
+
.stat-value.ok { color: var(--c-ok); }
|
|
6556
|
+
|
|
6557
|
+
/* \u2500\u2500 Meta table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6558
|
+
.meta-box {
|
|
6635
6559
|
background: var(--surface);
|
|
6636
6560
|
border: 1px solid var(--border);
|
|
6637
6561
|
border-radius: var(--radius);
|
|
6638
|
-
|
|
6639
|
-
|
|
6562
|
+
box-shadow: var(--shadow-sm);
|
|
6563
|
+
overflow: hidden;
|
|
6564
|
+
margin-bottom: 2rem;
|
|
6565
|
+
}
|
|
6566
|
+
.meta-row {
|
|
6640
6567
|
display: flex;
|
|
6641
|
-
align-items:
|
|
6642
|
-
gap:
|
|
6568
|
+
align-items: baseline;
|
|
6569
|
+
gap: 1rem;
|
|
6570
|
+
padding: 0.6rem 1rem;
|
|
6571
|
+
border-bottom: 1px solid var(--border-light);
|
|
6572
|
+
font-size: 0.875rem;
|
|
6643
6573
|
}
|
|
6644
|
-
.
|
|
6645
|
-
.
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
font-size: 1rem;
|
|
6652
|
-
font-weight: 600;
|
|
6653
|
-
margin: 0 0 0.85rem;
|
|
6654
|
-
color: var(--text);
|
|
6655
|
-
letter-spacing: -0.02em;
|
|
6574
|
+
.meta-row:last-child { border-bottom: none; }
|
|
6575
|
+
.meta-label {
|
|
6576
|
+
flex-shrink: 0;
|
|
6577
|
+
width: 7rem;
|
|
6578
|
+
color: var(--text-secondary);
|
|
6579
|
+
font-size: 0.78rem;
|
|
6580
|
+
font-weight: 500;
|
|
6656
6581
|
}
|
|
6657
|
-
.
|
|
6582
|
+
.meta-value { color: var(--text); }
|
|
6583
|
+
|
|
6584
|
+
/* \u2500\u2500 Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6585
|
+
.section { margin-top: 2.5rem; }
|
|
6586
|
+
.section-title {
|
|
6587
|
+
font-size: 0.8rem;
|
|
6588
|
+
font-weight: 700;
|
|
6589
|
+
letter-spacing: 0.06em;
|
|
6590
|
+
text-transform: uppercase;
|
|
6591
|
+
color: var(--text-secondary);
|
|
6592
|
+
margin-bottom: 0.75rem;
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
/* \u2500\u2500 Checks table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6596
|
+
.checks-table {
|
|
6658
6597
|
width: 100%;
|
|
6659
6598
|
border-collapse: collapse;
|
|
6660
|
-
font-size: 0.9rem;
|
|
6661
6599
|
background: var(--surface);
|
|
6600
|
+
border: 1px solid var(--border);
|
|
6662
6601
|
border-radius: var(--radius);
|
|
6663
6602
|
overflow: hidden;
|
|
6664
|
-
|
|
6665
|
-
|
|
6603
|
+
box-shadow: var(--shadow-sm);
|
|
6604
|
+
font-size: 0.855rem;
|
|
6666
6605
|
}
|
|
6667
|
-
.
|
|
6668
|
-
padding: 0.
|
|
6606
|
+
.checks-table thead th {
|
|
6607
|
+
padding: 0.5rem 0.75rem;
|
|
6669
6608
|
text-align: left;
|
|
6609
|
+
font-size: 0.7rem;
|
|
6610
|
+
font-weight: 600;
|
|
6611
|
+
letter-spacing: 0.06em;
|
|
6612
|
+
text-transform: uppercase;
|
|
6613
|
+
color: var(--muted);
|
|
6614
|
+
background: var(--surface-alt);
|
|
6670
6615
|
border-bottom: 1px solid var(--border);
|
|
6671
6616
|
}
|
|
6672
|
-
.
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6617
|
+
.checks-table tbody td {
|
|
6618
|
+
padding: 0.55rem 0.75rem;
|
|
6619
|
+
border-bottom: 1px solid var(--border-light);
|
|
6620
|
+
vertical-align: middle;
|
|
6621
|
+
}
|
|
6622
|
+
.checks-table tbody tr:last-child td { border-bottom: none; }
|
|
6623
|
+
.checks-table tbody tr:hover { background: #fafbfc; }
|
|
6624
|
+
.col-icon { width: 2.25rem; }
|
|
6625
|
+
.col-count, .col-time { color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
|
|
6626
|
+
.col-time { width: 5.5rem; }
|
|
6627
|
+
.col-count { width: 2.5rem; }
|
|
6628
|
+
|
|
6629
|
+
/* Status icons */
|
|
6630
|
+
.status-icon {
|
|
6631
|
+
display: inline-flex;
|
|
6632
|
+
align-items: center;
|
|
6633
|
+
justify-content: center;
|
|
6634
|
+
width: 1.3rem;
|
|
6635
|
+
height: 1.3rem;
|
|
6636
|
+
border-radius: 50%;
|
|
6637
|
+
font-size: 0.65rem;
|
|
6638
|
+
font-weight: 800;
|
|
6639
|
+
line-height: 1;
|
|
6640
|
+
}
|
|
6641
|
+
.status-icon.pass { background: var(--c-ok-bg); color: var(--c-ok); border: 1.5px solid #bbf7d0; }
|
|
6642
|
+
.status-icon.block { background: var(--c-block-bg); color: var(--c-block); border: 1.5px solid var(--c-block-border); }
|
|
6643
|
+
.status-icon.warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1.5px solid var(--c-warn-border); }
|
|
6644
|
+
.status-icon.skip { background: var(--surface-alt); color: var(--muted); border: 1.5px solid var(--border); font-size: 0.9rem; }
|
|
6645
|
+
|
|
6646
|
+
/* Check name + tooltip */
|
|
6647
|
+
.check-name { font-weight: 600; color: var(--text); cursor: default; }
|
|
6648
|
+
.check-tooltip {
|
|
6649
|
+
display: block;
|
|
6650
|
+
font-size: 0.75rem;
|
|
6651
|
+
color: var(--text-secondary);
|
|
6652
|
+
margin-top: 0.1rem;
|
|
6653
|
+
font-weight: 400;
|
|
6654
|
+
line-height: 1.4;
|
|
6678
6655
|
}
|
|
6679
|
-
|
|
6680
|
-
.
|
|
6656
|
+
.status-text { font-weight: 500; }
|
|
6657
|
+
.status-text.ok { color: var(--c-ok); }
|
|
6658
|
+
.status-text.danger { color: var(--c-block); }
|
|
6659
|
+
.status-text.caution { color: var(--c-warn); }
|
|
6660
|
+
.status-text.muted { color: var(--muted); font-weight: 400; font-size: 0.8rem; }
|
|
6661
|
+
|
|
6662
|
+
/* \u2500\u2500 Findings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6663
|
+
.accordion {
|
|
6681
6664
|
background: var(--surface);
|
|
6682
6665
|
border: 1px solid var(--border);
|
|
6683
6666
|
border-radius: var(--radius);
|
|
6667
|
+
box-shadow: var(--shadow-sm);
|
|
6684
6668
|
margin-bottom: 0.65rem;
|
|
6685
|
-
|
|
6669
|
+
overflow: hidden;
|
|
6686
6670
|
}
|
|
6687
|
-
.
|
|
6688
|
-
cursor: pointer;
|
|
6689
|
-
padding: 0.85rem 1rem;
|
|
6690
|
-
list-style: none;
|
|
6671
|
+
.accordion-header {
|
|
6691
6672
|
display: flex;
|
|
6692
6673
|
align-items: center;
|
|
6693
6674
|
justify-content: space-between;
|
|
6675
|
+
padding: 0.8rem 1rem;
|
|
6676
|
+
cursor: pointer;
|
|
6677
|
+
user-select: none;
|
|
6678
|
+
list-style: none;
|
|
6694
6679
|
font-weight: 600;
|
|
6695
6680
|
font-size: 0.9rem;
|
|
6696
6681
|
}
|
|
6697
|
-
.
|
|
6698
|
-
|
|
6699
|
-
.
|
|
6700
|
-
.
|
|
6701
|
-
|
|
6702
|
-
font-
|
|
6703
|
-
|
|
6704
|
-
color: var(--muted);
|
|
6705
|
-
background: #f1f5f9;
|
|
6706
|
-
padding: 0.15rem 0.5rem;
|
|
6682
|
+
.accordion-header::-webkit-details-marker { display: none; }
|
|
6683
|
+
details[open] > .accordion-header { border-bottom: 1px solid var(--border-light); }
|
|
6684
|
+
.accordion-label { display: flex; align-items: center; gap: 0.6rem; }
|
|
6685
|
+
.acc-count {
|
|
6686
|
+
font-size: 0.72rem;
|
|
6687
|
+
font-weight: 600;
|
|
6688
|
+
padding: 0.15rem 0.55rem;
|
|
6707
6689
|
border-radius: 999px;
|
|
6690
|
+
background: var(--surface-alt);
|
|
6691
|
+
color: var(--text-secondary);
|
|
6692
|
+
border: 1px solid var(--border);
|
|
6693
|
+
}
|
|
6694
|
+
.acc-count.danger { background: var(--c-block-bg); color: var(--c-block); border-color: var(--c-block-border); }
|
|
6695
|
+
.acc-count.caution { background: var(--c-warn-bg); color: var(--c-warn); border-color: var(--c-warn-border); }
|
|
6696
|
+
.accordion-body { padding: 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; }
|
|
6697
|
+
|
|
6698
|
+
/* Warning groups */
|
|
6699
|
+
.group { margin-bottom: 0.25rem; }
|
|
6700
|
+
.group-label {
|
|
6701
|
+
display: flex;
|
|
6702
|
+
align-items: center;
|
|
6703
|
+
gap: 0.5rem;
|
|
6704
|
+
padding: 0.4rem 0;
|
|
6705
|
+
margin-bottom: 0.45rem;
|
|
6708
6706
|
}
|
|
6707
|
+
.group-name { font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
6708
|
+
.group-count { font-size: 0.72rem; color: var(--muted); background: var(--surface-alt); padding: 0.1rem 0.45rem; border-radius: 999px; border: 1px solid var(--border); }
|
|
6709
|
+
|
|
6710
|
+
/* \u2500\u2500 Finding cards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6709
6711
|
.card {
|
|
6710
6712
|
border: 1px solid var(--border);
|
|
6711
|
-
border-radius:
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
background: #fafafa;
|
|
6713
|
+
border-radius: var(--radius-sm);
|
|
6714
|
+
background: var(--surface);
|
|
6715
|
+
padding: 0.85rem 1rem;
|
|
6715
6716
|
}
|
|
6716
|
-
.card
|
|
6717
|
-
.card.sev-
|
|
6718
|
-
.card.sev-
|
|
6719
|
-
.card
|
|
6720
|
-
.
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6717
|
+
.card.sev-block { border-left: 3px solid var(--c-block); background: var(--c-block-bg); border-top-color: var(--c-block-border); border-right-color: var(--c-block-border); border-bottom-color: var(--c-block-border); }
|
|
6718
|
+
.card.sev-warn { border-left: 3px solid var(--c-warn); }
|
|
6719
|
+
.card.sev-info { border-left: 3px solid var(--c-info); }
|
|
6720
|
+
.card-header { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.55rem; }
|
|
6721
|
+
.sev-badge {
|
|
6722
|
+
flex-shrink: 0;
|
|
6723
|
+
font-size: 0.65rem;
|
|
6724
|
+
font-weight: 700;
|
|
6725
|
+
letter-spacing: 0.05em;
|
|
6726
|
+
text-transform: uppercase;
|
|
6727
|
+
padding: 0.15rem 0.45rem;
|
|
6728
|
+
border-radius: 4px;
|
|
6729
|
+
margin-top: 0.15rem;
|
|
6730
|
+
}
|
|
6731
|
+
.sev-badge.sev-block { background: var(--c-block-bg); color: var(--c-block); border: 1px solid var(--c-block-border); }
|
|
6732
|
+
.sev-badge.sev-warn { background: var(--c-warn-bg); color: var(--c-warn); border: 1px solid var(--c-warn-border); }
|
|
6733
|
+
.sev-badge.sev-info { background: var(--c-info-bg); color: var(--c-info); border: 1px solid var(--c-info-border); }
|
|
6734
|
+
.card-message { font-size: 0.88rem; line-height: 1.5; color: var(--text); }
|
|
6735
|
+
.card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
6736
|
+
.tag {
|
|
6737
|
+
font-size: 0.72rem;
|
|
6738
|
+
padding: 0.1rem 0.45rem;
|
|
6739
|
+
border-radius: 4px;
|
|
6740
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6741
|
+
white-space: nowrap;
|
|
6742
|
+
overflow: hidden;
|
|
6743
|
+
text-overflow: ellipsis;
|
|
6744
|
+
max-width: 40ch;
|
|
6745
|
+
}
|
|
6746
|
+
.tag-check { background: #f0f0ff; color: #4338ca; border: 1px solid #e0e7ff; }
|
|
6747
|
+
.tag-rule { background: var(--surface-alt); color: var(--text-secondary); border: 1px solid var(--border); }
|
|
6748
|
+
.tag-file { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; max-width: 55ch; }
|
|
6749
|
+
.card-hint { font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; line-height: 1.45; }
|
|
6750
|
+
|
|
6751
|
+
/* Code blocks */
|
|
6752
|
+
.code-block {
|
|
6753
|
+
margin-top: 0.6rem;
|
|
6754
|
+
padding: 0.65rem 0.85rem;
|
|
6755
|
+
background: #f8fafc;
|
|
6735
6756
|
border: 1px solid var(--border);
|
|
6757
|
+
border-radius: var(--radius-sm);
|
|
6758
|
+
overflow-x: auto;
|
|
6759
|
+
font-size: 0.75rem;
|
|
6760
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6761
|
+
line-height: 1.55;
|
|
6762
|
+
white-space: pre;
|
|
6763
|
+
color: var(--text);
|
|
6764
|
+
}
|
|
6765
|
+
|
|
6766
|
+
/* Fix box */
|
|
6767
|
+
.fix-box {
|
|
6768
|
+
margin-top: 0.75rem;
|
|
6769
|
+
padding: 0.7rem 0.85rem;
|
|
6770
|
+
background: #fafafa;
|
|
6771
|
+
border: 1px dashed var(--border);
|
|
6772
|
+
border-radius: var(--radius-sm);
|
|
6736
6773
|
}
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6774
|
+
.fix-header {
|
|
6775
|
+
font-size: 0.7rem;
|
|
6776
|
+
font-weight: 700;
|
|
6777
|
+
text-transform: uppercase;
|
|
6778
|
+
letter-spacing: 0.06em;
|
|
6779
|
+
color: var(--accent);
|
|
6780
|
+
margin-bottom: 0.4rem;
|
|
6781
|
+
display: flex;
|
|
6782
|
+
align-items: center;
|
|
6783
|
+
gap: 0.4rem;
|
|
6784
|
+
}
|
|
6785
|
+
.badge-llm {
|
|
6786
|
+
font-size: 0.6rem;
|
|
6787
|
+
padding: 0.1rem 0.35rem;
|
|
6788
|
+
background: #eef2ff;
|
|
6789
|
+
color: var(--accent);
|
|
6790
|
+
border-radius: 4px;
|
|
6791
|
+
border: 1px solid #e0e7ff;
|
|
6792
|
+
}
|
|
6793
|
+
.fix-summary { font-size: 0.82rem; color: var(--text); white-space: pre-wrap; }
|
|
6794
|
+
.fix-disclaimer { font-size: 0.7rem; color: var(--muted); margin-top: 0.5rem; }
|
|
6795
|
+
|
|
6796
|
+
/* Misc */
|
|
6797
|
+
.empty-note { font-size: 0.875rem; color: var(--muted); padding: 0.25rem 0; }
|
|
6798
|
+
.muted { color: var(--muted); }
|
|
6799
|
+
.raw-pre {
|
|
6744
6800
|
white-space: pre-wrap;
|
|
6745
|
-
font-size: 0.
|
|
6801
|
+
font-size: 0.82rem;
|
|
6746
6802
|
background: var(--surface);
|
|
6747
6803
|
padding: 1rem;
|
|
6748
6804
|
border-radius: var(--radius);
|
|
6749
6805
|
border: 1px solid var(--border);
|
|
6750
|
-
|
|
6806
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
6807
|
+
line-height: 1.55;
|
|
6751
6808
|
}
|
|
6752
|
-
|
|
6753
|
-
|
|
6809
|
+
|
|
6810
|
+
/* \u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
6811
|
+
.footer {
|
|
6812
|
+
margin-top: 3.5rem;
|
|
6754
6813
|
padding-top: 1.25rem;
|
|
6755
6814
|
border-top: 1px solid var(--border);
|
|
6756
|
-
font-size: 0.
|
|
6815
|
+
font-size: 0.77rem;
|
|
6757
6816
|
color: var(--muted);
|
|
6817
|
+
display: flex;
|
|
6818
|
+
align-items: center;
|
|
6819
|
+
justify-content: space-between;
|
|
6820
|
+
flex-wrap: wrap;
|
|
6821
|
+
gap: 0.5rem;
|
|
6758
6822
|
}
|
|
6759
|
-
footer a { color: var(--accent); text-decoration: none; }
|
|
6760
|
-
footer a:hover { text-decoration: underline; }
|
|
6761
6823
|
</style>
|
|
6762
6824
|
</head>
|
|
6763
6825
|
<body>
|
|
6764
|
-
|
|
6826
|
+
<div class="wrap">
|
|
6827
|
+
|
|
6828
|
+
<header class="header">
|
|
6765
6829
|
<div class="brand">FrontGuard</div>
|
|
6766
|
-
<h1>Code review report</h1>
|
|
6767
|
-
<
|
|
6768
|
-
|
|
6769
|
-
<div class="
|
|
6770
|
-
<div class="
|
|
6771
|
-
<div class="
|
|
6772
|
-
<div class="
|
|
6830
|
+
<h1 class="page-title">Code review report</h1>
|
|
6831
|
+
<span class="risk-pill ${riskClass}"><span class="risk-icon">${riskIcon}</span> ${riskScore} risk</span>
|
|
6832
|
+
<div class="stats">
|
|
6833
|
+
<div class="stat"><span class="stat-label">Blocking</span><span class="stat-value ${blocks > 0 ? "danger" : "ok"}">${blocks}</span></div>
|
|
6834
|
+
<div class="stat"><span class="stat-label">Warnings</span><span class="stat-value ${warns > 0 ? "caution" : "ok"}">${warns}</span></div>
|
|
6835
|
+
<div class="stat"><span class="stat-label">Info</span><span class="stat-value">${infos}</span></div>
|
|
6836
|
+
<div class="stat"><span class="stat-label">Mode</span><span class="stat-value">${escapeHtml(modeLabel)}</span></div>
|
|
6773
6837
|
</div>
|
|
6774
6838
|
</header>
|
|
6775
6839
|
|
|
6776
|
-
<
|
|
6777
|
-
<
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
<tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
|
|
6782
|
-
${prBlock}
|
|
6783
|
-
</table>
|
|
6784
|
-
</section>
|
|
6840
|
+
<div class="meta-box">
|
|
6841
|
+
<div class="meta-row"><span class="meta-label">Stack</span><span class="meta-value">${escapeHtml(formatStackOneLiner(stack))}</span></div>
|
|
6842
|
+
${prRow}
|
|
6843
|
+
<div class="meta-row"><span class="meta-label">Gate mode</span><span class="meta-value">${escapeHtml(modeLabel)}</span></div>
|
|
6844
|
+
</div>
|
|
6785
6845
|
|
|
6786
6846
|
<section class="section">
|
|
6787
|
-
<h2 class="
|
|
6788
|
-
<table class="
|
|
6789
|
-
<thead
|
|
6790
|
-
|
|
6847
|
+
<h2 class="section-title">Checks</h2>
|
|
6848
|
+
<table class="checks-table">
|
|
6849
|
+
<thead>
|
|
6850
|
+
<tr>
|
|
6851
|
+
<th class="col-icon"></th>
|
|
6852
|
+
<th>Check</th>
|
|
6853
|
+
<th>Status</th>
|
|
6854
|
+
<th class="col-count">#</th>
|
|
6855
|
+
<th class="col-time">Time</th>
|
|
6856
|
+
</tr>
|
|
6857
|
+
</thead>
|
|
6858
|
+
<tbody>
|
|
6859
|
+
${checkRows}
|
|
6860
|
+
</tbody>
|
|
6791
6861
|
</table>
|
|
6792
6862
|
</section>
|
|
6793
6863
|
|
|
6794
6864
|
<section class="section">
|
|
6795
|
-
<h2 class="
|
|
6796
|
-
|
|
6797
|
-
<details class="
|
|
6798
|
-
|
|
6865
|
+
<h2 class="section-title">Findings</h2>
|
|
6866
|
+
|
|
6867
|
+
<details class="accordion" ${blocks > 0 ? "open" : ""}>
|
|
6868
|
+
<summary class="accordion-header">
|
|
6869
|
+
<span class="accordion-label">Blocking <span class="acc-count ${blocks > 0 ? "danger" : ""}">${blocks}</span></span>
|
|
6870
|
+
</summary>
|
|
6871
|
+
<div class="accordion-body">${blockHtml}</div>
|
|
6872
|
+
</details>
|
|
6873
|
+
|
|
6874
|
+
<details class="accordion" ${warns > 0 && blocks === 0 ? "open" : ""}>
|
|
6875
|
+
<summary class="accordion-header">
|
|
6876
|
+
<span class="accordion-label">Warnings <span class="acc-count ${warns > 0 ? "caution" : ""}">${warns}</span></span>
|
|
6877
|
+
</summary>
|
|
6878
|
+
<div class="accordion-body">${warnsHtml}</div>
|
|
6879
|
+
</details>
|
|
6880
|
+
|
|
6881
|
+
<details class="accordion">
|
|
6882
|
+
<summary class="accordion-header">
|
|
6883
|
+
<span class="accordion-label">Info <span class="acc-count">${infos}</span></span>
|
|
6884
|
+
</summary>
|
|
6885
|
+
<div class="accordion-body">${infoHtml}</div>
|
|
6886
|
+
</details>
|
|
6799
6887
|
</section>
|
|
6800
6888
|
|
|
6801
|
-
${
|
|
6889
|
+
${appendixHtml}
|
|
6802
6890
|
|
|
6803
|
-
<footer>
|
|
6804
|
-
<
|
|
6891
|
+
<footer class="footer">
|
|
6892
|
+
<span>Generated by <strong>FrontGuard</strong> \xB7 ${ts2}</span>
|
|
6893
|
+
<span>Open in any browser \xB7 static report</span>
|
|
6805
6894
|
</footer>
|
|
6895
|
+
|
|
6896
|
+
</div>
|
|
6806
6897
|
</body>
|
|
6807
6898
|
</html>`;
|
|
6808
6899
|
}
|
|
@@ -7001,76 +7092,52 @@ function formatMarkdown(p2) {
|
|
|
7001
7092
|
if (lineA !== lineB) return lineA - lineB;
|
|
7002
7093
|
return a3.f.message.localeCompare(b3.f.message);
|
|
7003
7094
|
});
|
|
7004
|
-
sb.push("##
|
|
7095
|
+
sb.push("## FrontGuard report");
|
|
7005
7096
|
sb.push("");
|
|
7006
7097
|
const modeLabel = mode === "enforce" ? "enforce" : "warn only";
|
|
7007
7098
|
const badgeLine = [
|
|
7008
|
-
mdShield("Risk
|
|
7009
|
-
mdShield("
|
|
7099
|
+
mdShield("Risk", "risk", riskScore, riskShieldColor(riskScore)),
|
|
7100
|
+
mdShield("Mode", "mode", modeLabel, modeShieldColor(mode)),
|
|
7010
7101
|
mdShield("Blocking", "blocking", String(blocks), countShieldColor("block", blocks)),
|
|
7011
7102
|
mdShield("Warnings", "warnings", String(warns), countShieldColor("warn", warns)),
|
|
7012
|
-
mdShield("Info", "info
|
|
7103
|
+
mdShield("Info", "info", String(infos), countShieldColor("info", infos))
|
|
7013
7104
|
].join(" ");
|
|
7014
7105
|
sb.push(badgeLine);
|
|
7015
7106
|
sb.push("");
|
|
7016
|
-
sb.push("---");
|
|
7017
|
-
sb.push("");
|
|
7018
|
-
sb.push("### \u{1F4CC} Snapshot");
|
|
7019
|
-
sb.push("");
|
|
7020
7107
|
sb.push("| | |");
|
|
7021
7108
|
sb.push("|:--|:--|");
|
|
7022
|
-
sb.push(`|
|
|
7023
|
-
sb.push(
|
|
7024
|
-
|
|
7025
|
-
);
|
|
7026
|
-
sb.push(`| **Stack detected** | ${formatStackOneLiner(stack)} |`);
|
|
7109
|
+
sb.push(`| Risk | ${riskEmoji(riskScore)} **${riskScore}** |`);
|
|
7110
|
+
sb.push(`| Mode | ${mode === "enforce" ? "\u{1F512} Enforce" : "Warn only"} |`);
|
|
7111
|
+
sb.push(`| Stack | ${formatStackOneLiner(stack)} |`);
|
|
7027
7112
|
if (pr && lines != null) {
|
|
7028
|
-
sb.push(
|
|
7029
|
-
`| **PR size** | \u{1F4CF} **${lines}** LOC ( +${pr.additions} / \u2212${pr.deletions} ) \xB7 **${pr.changedFiles}** files |`
|
|
7030
|
-
);
|
|
7113
|
+
sb.push(`| PR size | ${lines} lines (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files |`);
|
|
7031
7114
|
}
|
|
7032
7115
|
sb.push("");
|
|
7033
7116
|
if (showAiAssistedBanner && pr?.aiAssisted) {
|
|
7034
7117
|
sb.push(
|
|
7035
|
-
">
|
|
7118
|
+
"> **AI-assisted PR** \u2014 stricter static checks active (security, secrets, `any` deltas may escalate)."
|
|
7036
7119
|
);
|
|
7037
7120
|
sb.push("");
|
|
7038
7121
|
}
|
|
7039
|
-
sb.push("> **How to read this report**");
|
|
7040
|
-
sb.push(">");
|
|
7041
|
-
sb.push("> | Symbol | Meaning |");
|
|
7042
|
-
sb.push("> |:--|:--|");
|
|
7043
|
-
sb.push("> | \u{1F7E2} | Check passed or skipped cleanly |");
|
|
7044
|
-
sb.push("> | \u{1F7E1} | Warnings only \u2014 review recommended |");
|
|
7045
|
-
sb.push("> | \u{1F534} | Blocking (`block`) severity present |");
|
|
7046
|
-
sb.push("> | \u23ED\uFE0F | Check skipped (see reason in table) |");
|
|
7047
|
-
sb.push(">");
|
|
7048
|
-
sb.push(
|
|
7049
|
-
"> Paths in findings are **relative to the repo root**. Each issue below has a small field table and optional detail."
|
|
7050
|
-
);
|
|
7051
|
-
sb.push("");
|
|
7052
7122
|
sb.push("---");
|
|
7053
7123
|
sb.push("");
|
|
7054
|
-
sb.push("###
|
|
7124
|
+
sb.push("### Checks");
|
|
7055
7125
|
sb.push("");
|
|
7056
|
-
sb.push("| | Check |
|
|
7057
|
-
sb.push("
|
|
7126
|
+
sb.push("| | Check | Issues | Time |");
|
|
7127
|
+
sb.push("|:--:|:--|:-:|--:|");
|
|
7058
7128
|
for (const r4 of results) {
|
|
7059
7129
|
const he2 = healthEmojiForCheck(r4);
|
|
7060
7130
|
let status;
|
|
7061
7131
|
if (r4.skipped) {
|
|
7062
7132
|
const why = r4.skipped.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
7063
|
-
const short = why.length >
|
|
7064
|
-
status = `\
|
|
7133
|
+
const short = why.length > 80 ? `${why.slice(0, 77)}\u2026` : why;
|
|
7134
|
+
status = `\u2014 _${short}_`;
|
|
7065
7135
|
} else if (r4.findings.length === 0) {
|
|
7066
|
-
status = "\
|
|
7136
|
+
status = "\u2014";
|
|
7067
7137
|
} else {
|
|
7068
|
-
status =
|
|
7138
|
+
status = String(r4.findings.length);
|
|
7069
7139
|
}
|
|
7070
|
-
|
|
7071
|
-
sb.push(
|
|
7072
|
-
`| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration2(r4.durationMs)} |`
|
|
7073
|
-
);
|
|
7140
|
+
sb.push(`| ${he2} | \`${r4.checkId}\` | ${status} | ${formatDuration2(r4.durationMs)} |`);
|
|
7074
7141
|
}
|
|
7075
7142
|
sb.push("");
|
|
7076
7143
|
const blockFindings = sortWithCwd(
|
|
@@ -7078,10 +7145,10 @@ function formatMarkdown(p2) {
|
|
|
7078
7145
|
(r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
|
|
7079
7146
|
)
|
|
7080
7147
|
);
|
|
7081
|
-
sb.push(`###
|
|
7148
|
+
sb.push(`### Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}`);
|
|
7082
7149
|
sb.push("");
|
|
7083
7150
|
if (blockFindings.length === 0) {
|
|
7084
|
-
sb.push("
|
|
7151
|
+
sb.push("_No blocking findings._");
|
|
7085
7152
|
} else {
|
|
7086
7153
|
for (const { r: r4, f: f4 } of blockFindings) {
|
|
7087
7154
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7089,11 +7156,11 @@ function formatMarkdown(p2) {
|
|
|
7089
7156
|
sb.push("");
|
|
7090
7157
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7091
7158
|
sb.push("");
|
|
7092
|
-
sb.push(`|
|
|
7159
|
+
sb.push(`| | |`);
|
|
7093
7160
|
sb.push(`|:--|:--|`);
|
|
7094
|
-
sb.push(`|
|
|
7095
|
-
sb.push(`|
|
|
7096
|
-
if (d3.file) sb.push(`|
|
|
7161
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7162
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7163
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7097
7164
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7098
7165
|
appendSuggestedFix(sb, cwd, f4);
|
|
7099
7166
|
sb.push("");
|
|
@@ -7105,12 +7172,10 @@ function formatMarkdown(p2) {
|
|
|
7105
7172
|
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
7106
7173
|
)
|
|
7107
7174
|
);
|
|
7108
|
-
sb.push(
|
|
7109
|
-
`### \u26A0\uFE0F Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"} (by check)`
|
|
7110
|
-
);
|
|
7175
|
+
sb.push(`### Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"}`);
|
|
7111
7176
|
sb.push("");
|
|
7112
7177
|
if (warnFindings.length === 0) {
|
|
7113
|
-
sb.push("
|
|
7178
|
+
sb.push("_No warnings._");
|
|
7114
7179
|
} else {
|
|
7115
7180
|
const byCheck = /* @__PURE__ */ new Map();
|
|
7116
7181
|
for (const item of warnFindings) {
|
|
@@ -7121,7 +7186,7 @@ function formatMarkdown(p2) {
|
|
|
7121
7186
|
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
7122
7187
|
for (const checkId of checkOrder) {
|
|
7123
7188
|
const group = sortWithCwd(byCheck.get(checkId));
|
|
7124
|
-
sb.push(`####
|
|
7189
|
+
sb.push(`#### \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
|
|
7125
7190
|
sb.push("");
|
|
7126
7191
|
for (const { r: r4, f: f4 } of group) {
|
|
7127
7192
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7129,11 +7194,11 @@ function formatMarkdown(p2) {
|
|
|
7129
7194
|
sb.push("");
|
|
7130
7195
|
sb.push(`##### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7131
7196
|
sb.push("");
|
|
7132
|
-
sb.push(`|
|
|
7197
|
+
sb.push(`| | |`);
|
|
7133
7198
|
sb.push(`|:--|:--|`);
|
|
7134
|
-
sb.push(`|
|
|
7135
|
-
sb.push(`|
|
|
7136
|
-
if (d3.file) sb.push(`|
|
|
7199
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7200
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7201
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7137
7202
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7138
7203
|
appendSuggestedFix(sb, cwd, f4);
|
|
7139
7204
|
sb.push("");
|
|
@@ -7146,10 +7211,10 @@ function formatMarkdown(p2) {
|
|
|
7146
7211
|
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
7147
7212
|
)
|
|
7148
7213
|
);
|
|
7149
|
-
sb.push(`###
|
|
7214
|
+
sb.push(`### Info \u2014 ${infos} item${infos === 1 ? "" : "s"}`);
|
|
7150
7215
|
sb.push("");
|
|
7151
7216
|
if (infoFindings.length === 0) {
|
|
7152
|
-
sb.push("
|
|
7217
|
+
sb.push("_No info notes._");
|
|
7153
7218
|
} else {
|
|
7154
7219
|
for (const { r: r4, f: f4 } of infoFindings) {
|
|
7155
7220
|
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
@@ -7157,11 +7222,11 @@ function formatMarkdown(p2) {
|
|
|
7157
7222
|
sb.push("");
|
|
7158
7223
|
sb.push(`#### ${findingTitleLine(d3.file, d3.message)}`);
|
|
7159
7224
|
sb.push("");
|
|
7160
|
-
sb.push(`|
|
|
7225
|
+
sb.push(`| | |`);
|
|
7161
7226
|
sb.push(`|:--|:--|`);
|
|
7162
|
-
sb.push(`|
|
|
7163
|
-
sb.push(`|
|
|
7164
|
-
if (d3.file) sb.push(`|
|
|
7227
|
+
sb.push(`| Check | \`${r4.checkId}\` |`);
|
|
7228
|
+
sb.push(`| Rule | \`${f4.id}\` |`);
|
|
7229
|
+
if (d3.file) sb.push(`| File | \`${d3.file}\` |`);
|
|
7165
7230
|
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
7166
7231
|
appendSuggestedFix(sb, cwd, f4);
|
|
7167
7232
|
sb.push("");
|
|
@@ -7169,7 +7234,7 @@ function formatMarkdown(p2) {
|
|
|
7169
7234
|
}
|
|
7170
7235
|
sb.push("");
|
|
7171
7236
|
if (llmAppendix?.trim()) {
|
|
7172
|
-
sb.push("###
|
|
7237
|
+
sb.push("### Appendix");
|
|
7173
7238
|
sb.push("");
|
|
7174
7239
|
sb.push(llmAppendix.trim());
|
|
7175
7240
|
sb.push("");
|
|
@@ -7177,40 +7242,45 @@ function formatMarkdown(p2) {
|
|
|
7177
7242
|
sb.push("---");
|
|
7178
7243
|
sb.push("");
|
|
7179
7244
|
sb.push(
|
|
7180
|
-
|
|
7181
|
-
"Generated by",
|
|
7182
|
-
"report",
|
|
7183
|
-
"FrontGuard",
|
|
7184
|
-
"blueviolet"
|
|
7185
|
-
)
|
|
7186
|
-
);
|
|
7187
|
-
sb.push("");
|
|
7188
|
-
sb.push(
|
|
7189
|
-
"_Configure checks in `frontguard.config.mjs` (or `.cjs` / `.js`) \xB7 [Shields.io](https://shields.io) badge images load in Bitbucket PR comments (HTML tags like `<details>` are not supported there)._"
|
|
7245
|
+
"_Report by [FrontGuard](https://github.com/flipkart-incubator/frontguard) \xB7 Configure in `frontguard.config.mjs`_"
|
|
7190
7246
|
);
|
|
7191
7247
|
return sb.join("\n");
|
|
7192
7248
|
}
|
|
7193
7249
|
function formatConsole(p2) {
|
|
7194
7250
|
const { riskScore, mode, stack, pr, results, warns, infos, blocks } = p2;
|
|
7251
|
+
const W3 = 56;
|
|
7252
|
+
const pad = (s3, n3) => s3.slice(0, n3).padEnd(n3);
|
|
7253
|
+
const row = (label, val) => `\u2502 ${import_picocolors.default.dim(label.padEnd(7))} ${pad(val, W3 - 10)} \u2502`;
|
|
7254
|
+
const riskColored = riskScore === "HIGH" ? import_picocolors.default.red(import_picocolors.default.bold(riskScore)) : riskScore === "MEDIUM" ? import_picocolors.default.yellow(import_picocolors.default.bold(riskScore)) : import_picocolors.default.green(import_picocolors.default.bold(riskScore));
|
|
7255
|
+
const stackStr = [
|
|
7256
|
+
stack.hasNext && "Next.js",
|
|
7257
|
+
stack.hasReactNative && "React Native",
|
|
7258
|
+
!stack.hasNext && !stack.hasReactNative && stack.hasReact && "React",
|
|
7259
|
+
stack.hasTypeScript ? "TypeScript" : "JavaScript",
|
|
7260
|
+
stack.isMonorepo && "monorepo"
|
|
7261
|
+
].filter(Boolean).join(" \xB7 ");
|
|
7195
7262
|
const lines = [];
|
|
7196
|
-
|
|
7197
|
-
lines.push(
|
|
7198
|
-
lines.push(
|
|
7199
|
-
lines.push(
|
|
7200
|
-
|
|
7201
|
-
);
|
|
7263
|
+
const border = "\u2500".repeat(W3);
|
|
7264
|
+
lines.push(import_picocolors.default.dim(`\u250C${border}\u2510`));
|
|
7265
|
+
lines.push(row("Risk", riskColored));
|
|
7266
|
+
lines.push(row("Mode", mode === "enforce" ? import_picocolors.default.yellow("enforce") : "warn only"));
|
|
7267
|
+
lines.push(row("Stack", stackStr));
|
|
7202
7268
|
if (pr) {
|
|
7203
|
-
const sz = `${pr.additions + pr.deletions} lines
|
|
7204
|
-
lines.push(
|
|
7269
|
+
const sz = `${pr.additions + pr.deletions} lines +${pr.additions} / -${pr.deletions} \xB7 ${pr.changedFiles} files`;
|
|
7270
|
+
lines.push(row("PR", sz));
|
|
7205
7271
|
}
|
|
7206
|
-
lines.push(
|
|
7207
|
-
const statusLine = blocks > 0 ? import_picocolors.default.red(`\
|
|
7208
|
-
lines.push(`\u2502 ${statusLine
|
|
7272
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
7273
|
+
const statusLine = blocks > 0 ? import_picocolors.default.red(`\u2715 ${blocks} blocking \xB7 ${warns} warnings \xB7 ${infos} info`) : warns > 0 ? import_picocolors.default.yellow(`! ${warns} warnings \xB7 ${infos} info`) : import_picocolors.default.green("\u2713 All checks passed");
|
|
7274
|
+
lines.push(`\u2502 ${pad(statusLine, W3 - 2)} \u2502`);
|
|
7275
|
+
lines.push(import_picocolors.default.dim(`\u251C${border}\u2524`));
|
|
7209
7276
|
for (const r4 of results) {
|
|
7210
|
-
const
|
|
7211
|
-
|
|
7277
|
+
const icon = r4.skipped ? import_picocolors.default.dim("\u2013") : r4.findings.length === 0 ? import_picocolors.default.green("\u2713") : r4.findings.some((f4) => f4.severity === "block") ? import_picocolors.default.red("\u2715") : import_picocolors.default.yellow("!");
|
|
7278
|
+
const countStr = r4.skipped ? import_picocolors.default.dim("skipped") : r4.findings.length === 0 ? import_picocolors.default.dim("pass") : r4.findings.length === 1 ? `${r4.findings.length} issue` : `${r4.findings.length} issues`;
|
|
7279
|
+
const name = r4.checkId.padEnd(22);
|
|
7280
|
+
const label = ` ${icon} ${name} ${countStr}`;
|
|
7281
|
+
lines.push(`\u2502${pad(label, W3 + 1)}\u2502`);
|
|
7212
7282
|
}
|
|
7213
|
-
lines.push(import_picocolors.default.
|
|
7283
|
+
lines.push(import_picocolors.default.dim(`\u2514${border}\u2518`));
|
|
7214
7284
|
return lines.join("\n");
|
|
7215
7285
|
}
|
|
7216
7286
|
|