Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ For a quick overview, see the [README](./README.md). This document covers everyt
- [Troubleshooting: registry, auth, and network](#troubleshooting-registry-auth-and-network)
- [Privacy & Security](#privacy--security)
- [Exit Codes](#exit-codes)
- [Warning codes](#warning-codes)
- [Programmatic API](#programmatic-api)

---
Expand Down Expand Up @@ -5374,6 +5375,50 @@ exits `6` rather than quietly producing a review no model contributed to.

---

## Warning codes

A warning means the command continued. The process exit code stays the one in [Exit Codes](#exit-codes). Each published code keeps its meaning. A later release may add a code. It does not rename or reuse a code that has shipped. A code never contains a path, a secret, or file contents. When several warnings fire, JSON lists them by code, then by message.

Stderr lines look like `warning [VG_WARN_PARSE_FAILED]: …`. The code is the token a CI check or an assistant asserts on. The sentence after the code can be reworded.

`vg build --json` keeps `warnings` as strings and adds `codedWarnings`: an array of `{ code, message }` in that same order. Each string in `warnings` ends with ` [CODE]`. `codedWarnings` is left out when there are no warnings.

`vg scan --format json` adds `degradations` when a path was skipped or a baseline file could not be read. The field is left out when there are none. `vg report` prints those same rows.

`vg sbom export` prints the code on stderr. CycloneDX adds a `vibgrate:warningCode` property next to the existing warning property. SPDX adds an annotation whose comment is `warningCode=VG_WARN_…`. The warning sentences already stored on the document stay as they are.

License findings keep the rule id `vibgrate/license-parse-failed` and also carry `warnCode`. CVSS findings keep `cvss-vector-parse-failed` and also carry `warnCode`.

A truncated or invalid lockfile stops the command. Warning codes cover conditions that let the command continue.

| Code | Command | Meaning |
| --- | --- | --- |
| `VG_WARN_PARSE_FAILED` | `vg build` | A source file failed to parse. The map continues without its symbols. |
| `VG_WARN_BUILD_FILE_OVERSIZE` | `vg build` | A file exceeded the per-file size cap and was left out of the map. |
| `VG_WARN_TSC_RESOLVER_SKIPPED` | `vg build` | The TypeScript resolver was skipped because the corpus exceeded its file cap. |
| `VG_WARN_YAML_PARSE_FAILED` | `vg build` | YAML for an infrastructure file failed to parse. |
| `VG_WARN_YAML_DOCUMENT_SKIPPED` | `vg build` | A YAML document in a stream had errors and was skipped. |
| `VG_WARN_YAML_DOCUMENT_UNMATERIALISED` | `vg build` | A YAML document could not be materialised. |
| `VG_WARN_TOOLCHAIN_EXTRACTION_FAILED` | `vg build` | An infrastructure extractor failed. That file contributes no structure. |
| `VG_WARN_TOOLCHAIN_NODE_CAP` | `vg build` | An extractor stopped at the per-file node cap. |
| `VG_WARN_HCL_GRAMMAR_UNAVAILABLE` | `vg build` | The HCL grammar was unavailable, so no Terraform structure was extracted. |
| `VG_WARN_HCL_NO_TREE` | `vg build` | HCL parse produced no tree. |
| `VG_WARN_HCL_PARTIAL` | `vg build` | HCL parse recovered from a syntax error. Extraction may be partial. |
| `VG_WARN_WORKFLOW_STEP_CAP` | `vg build` | A workflow job listed more steps than the extractor enumerates. |
| `VG_WARN_SCAN_PATH_SKIPPED` | `vg scan` | A scan path timed out and was skipped. |
| `VG_WARN_SCAN_FILE_OVERSIZE` | `vg scan` | A scan file exceeded the size cap and was skipped. |
| `VG_WARN_BASELINE_UNREADABLE` | `vg scan` | A baseline file could not be read. The scan continues without a comparison. |
| `VG_WARN_LICENSE_UNPARSEABLE` | `vg scan`, `vg sbom` | A declared license string could not be resolved to SPDX. |
| `VG_WARN_LICENSE_UNREPRESENTABLE` | `vg sbom` | A declared license cannot be represented in an SBOM. |
| `VG_WARN_CVSS_UNPARSEABLE` | `vg scan` | A CVSS vector was present and could not be parsed. |
| `VG_WARN_PURL_UNAVAILABLE` | `vg sbom` | A package URL could not be formed. The component is included without a purl. |
| `VG_WARN_SBOM_LOSSY_EDGES` | `vg sbom` | Merge dropped a different dependency list for one package. |
| `VG_WARN_SBOM_LOSSY_MANIFEST` | `vg sbom` | Merge dropped differing manifest metadata for one package. |
| `VG_WARN_SBOM_UNKNOWN_ECOSYSTEM` | `vg sbom` | Merge recorded an unknown ecosystem as npm. |
| `VG_WARN_SBOM_UNTRACKED_EDGES` | `vg sbom` | A lockfile format does not record dependency edges. |

---

## Programmatic API

The package exports its core types for programmatic use:
Expand Down
9 changes: 9 additions & 0 deletions src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { UnsafeRootError } from '../core-open/utils/root-safety.js';
import { CliError, ExitCode, usageError } from '../util/exit.js';
import { resolveSelfJsEntry } from '../util/cli-invocation.js';
import { c, info, out, json } from '../util/output.js';
import { formatWarningLine } from '../core-open/warnings.js';
import { printLogo } from '../util/logo.js';
import { ProgressBar } from '../util/progress.js';
import { applyGlobalOptions, readGlobal, type GlobalOpts } from '../cli-options.js';
Expand Down Expand Up @@ -281,6 +282,7 @@ export async function runBuild(
attestation,
timingMs: result.timing.totalMs,
warnings: result.warnings,
...(result.codedWarnings.length > 0 ? { codedWarnings: result.codedWarnings } : {}),
activity: activity.toJSON(),
});
return;
Expand Down Expand Up @@ -339,6 +341,13 @@ export async function runBuild(
.join(' ');
info(` → ${artifactList}`);
if (result.warnings.length) {
for (const warning of result.codedWarnings) {
info(c.yellow(` ${formatWarningLine(warning)}`));
}
const stamped = new Set(result.codedWarnings.map((warning) => `${warning.message} [${warning.code}]`));
for (const line of result.warnings) {
if (!stamped.has(line)) info(c.yellow(` warning: ${line}`));
}
info(c.yellow(` ${result.warnings.length} parse warning(s) — run with --json for detail`));
}
if (attestation) {
Expand Down
11 changes: 10 additions & 1 deletion src/core-open/formatters/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// and re-run the vendor script. Apache-2.0.
import type { ScanArtifact } from '../types.js';
import { baselineSuppressionLabel } from '../baseline-suppressions.js';
import { findingRuleLabel, formatDegradationLines } from '../warnings.js';
import { securityPacksLabel } from './text.js';

/** Rows shown before the infrastructure-findings table is cut with an "… N more" line. */
Expand Down Expand Up @@ -173,6 +174,14 @@ export function formatMarkdown(artifact: ScanArtifact): string {
}
lines.push('');
}
const degradationLines = formatDegradationLines(artifact.degradations);
if (degradationLines.length > 0) {
lines.push('## Degradations');
lines.push('');
for (const line of degradationLines) lines.push(`- ${line}`);
lines.push('');
}

// Findings
if (artifact.findings.length > 0) {
lines.push('## Findings');
Expand All @@ -181,7 +190,7 @@ export function formatMarkdown(artifact: ScanArtifact): string {
lines.push(`|-------|------|---------|----------|`);
for (const f of artifact.findings) {
const emoji = f.level === 'error' ? '🔴' : f.level === 'warning' ? '🟡' : '🔵';
lines.push(`| ${emoji} ${f.level} | ${f.ruleId} | ${f.message} | ${f.location} |`);
lines.push(`| ${emoji} ${f.level} | ${findingRuleLabel(f.ruleId, f.details)} | ${f.message} | ${f.location} |`);
}
lines.push('');
}
Expand Down
10 changes: 9 additions & 1 deletion src/core-open/formatters/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import chalk from 'chalk';
import type { ScanArtifact, BillingSummary, ExtendedScanResults, InventoryItem, ServiceDependencyItem, ArchitectureResult, SecurityFinding, SecuritySection } from '../types.js';
import { baselineSuppressionLabel } from '../baseline-suppressions.js';
import { humanFindingText } from './fix-hint.js';
import { findingRuleLabel, formatDegradationLines } from '../warnings.js';
import { driftBar } from '../ui/bar.js';
import { titleBox, panelBox } from '../ui/box.js';

Expand Down Expand Up @@ -116,6 +117,13 @@ export function formatText(artifact: ScanArtifact, opts: FormatTextOptions = {})
lines.push(...formatExtended(artifact.extended));
}

const degradationLines = formatDegradationLines(artifact.degradations);
if (degradationLines.length > 0) {
lines.push(chalk.bold.underline(' Degradations'));
for (const line of degradationLines) lines.push(chalk.yellow(` ${line}`));
lines.push('');
}

// Findings
if (artifact.findings.length > 0) {
const errors = artifact.findings.filter((f) => f.level === 'error');
Expand All @@ -132,7 +140,7 @@ export function formatText(artifact: ScanArtifact, opts: FormatTextOptions = {})
const icon = f.level === 'error' ? chalk.red('✖') : f.level === 'warning' ? chalk.yellow('⚠') : chalk.blue('ℹ');
const shown = humanFindingText(f);
lines.push(` ${icon} ${shown.message}`);
lines.push(chalk.dim(` ${f.ruleId} in ${f.location}`));
lines.push(chalk.dim(` ${findingRuleLabel(f.ruleId, f.details)} in ${f.location}`));
if (shown.hint) lines.push(chalk.dim(` ${shown.hint}`));
}
lines.push('');
Expand Down
4 changes: 4 additions & 0 deletions src/core-open/licenses/diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { isExplicitUnknownLicense, normalizeLicense } from './normalize.js';
import { parseLicenseExpression } from './spdx-expression.js';
import { WARNING_CODES, type WarningCode } from '../warnings.js';

/** Stable code for scan, report, SARIF, and SBOM. Identical for every failure. */
export const LICENSE_PARSE_FAILED = 'vibgrate/license-parse-failed';
Expand All @@ -28,6 +29,8 @@ export const LICENSE_RAW_LIMIT = 120;

export interface LicenseParseDiagnostic {
code: typeof LICENSE_PARSE_FAILED;
/** Degrade-and-continue warning code. `code` stays the finding rule id. */
warnCode: WarningCode;
/** Actionable, deterministic text. No file body and no credential. */
message: string;
/** Manifest-relative path the declaration was read from. */
Expand Down Expand Up @@ -109,6 +112,7 @@ export function licenseParseDiagnostic(
'Replace it with a canonical SPDX id or expression, or NOASSERTION if the license is intentionally unknown.';
return {
code: LICENSE_PARSE_FAILED,
warnCode: WARNING_CODES.LICENSE_UNPARSEABLE,
message,
location: manifestPath,
raw: shown,
Expand Down
56 changes: 40 additions & 16 deletions src/core-open/run-core-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ import type {
ScanArtifact, ScanOptions, ProjectScan, ExtendedScanResults, RepositoryInfo, SolutionScan,
VibgrateConfig, Finding,
} from './types.js';
import {
codedWarning,
formatWarningLine,
sortCodedWarnings,
warningPathLabel,
WARNING_CODES,
type CodedWarning,
} from './warnings.js';
import type { RuntimeCatalog } from './runtimes/types.js';

/**
Expand Down Expand Up @@ -762,15 +770,19 @@ export async function runCoreScan(

progress.finish();

const stuckPaths = fileCache.stuckPaths;
const skippedLarge = fileCache.skippedLargeFiles;
const degradations: CodedWarning[] = [];
const stuckPaths = [...fileCache.stuckPaths].sort((a, b) => a.localeCompare(b));
const skippedLarge = [...fileCache.skippedLargeFiles].sort((a, b) => a.localeCompare(b));
const timeoutSeconds = Math.round(projectScanTimeoutMs / 1000);

if (stuckPaths.length > 0) {
console.log(
chalk.yellow(`\n⚠ ${stuckPaths.length} path${stuckPaths.length === 1 ? '' : 's'} timed out (>${Math.round(projectScanTimeoutMs / 1000)}s) and ${stuckPaths.length === 1 ? 'was' : 'were'} skipped:`),
);
for (const d of stuckPaths) {
console.log(chalk.dim(` → ${d}`));
for (const rel of stuckPaths) {
degradations.push(
codedWarning(
WARNING_CODES.SCAN_PATH_SKIPPED,
`${warningPathLabel(rel)} timed out (>${timeoutSeconds}s) and was skipped`,
),
);
}
const newExcludes = stuckPaths.map((d) => `${d}/**`);
const updated = await appendExcludePatterns(rootDir, newExcludes);
Expand All @@ -782,14 +794,13 @@ export async function runCoreScan(
if (skippedLarge.length > 0) {
const sizeLimit = config.maxFileSizeToScan ?? 5_242_880;
const sizeMB = (sizeLimit / 1_048_576).toFixed(0);
console.log(
chalk.yellow(`\n⚠ ${skippedLarge.length} file${skippedLarge.length === 1 ? '' : 's'} skipped (>${sizeMB} MB):`),
);
for (const f of skippedLarge.slice(0, 10)) {
console.log(chalk.dim(` → ${f}`));
}
if (skippedLarge.length > 10) {
console.log(chalk.dim(` … and ${skippedLarge.length - 10} more`));
for (const rel of skippedLarge) {
degradations.push(
codedWarning(
WARNING_CODES.SCAN_FILE_OVERSIZE,
`${warningPathLabel(rel)} skipped — larger than ${sizeMB} MB`,
),
);
}
}

Expand Down Expand Up @@ -845,11 +856,24 @@ export async function runCoreScan(
const baselineFindings = Array.isArray(baseline.findings) ? baseline.findings : [];
artifact.baselineComparison = compareBaselineFindings(artifact.findings, baselineFindings);
} catch {
console.error(chalk.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
degradations.push(
codedWarning(
WARNING_CODES.BASELINE_UNREADABLE,
`Could not read baseline file: ${warningPathLabel(path.basename(baselinePath))}`,
),
);
}
}
}

const sortedDegradations = sortCodedWarnings(degradations);
if (sortedDegradations.length > 0) {
artifact.degradations = sortedDegradations;
for (const warning of sortedDegradations) {
console.error(chalk.yellow(formatWarningLine(warning)));
}
}

if (!opts.noLocalArtifacts && !maxPrivacyMode) {
const vibgrateDir = path.join(rootDir, '.vibgrate');
await ensureDir(vibgrateDir);
Expand Down
3 changes: 2 additions & 1 deletion src/core-open/scoring/cvss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// and re-run the vendor script. Apache-2.0.
import type { CvssDiagnostic, VulnSeverity } from '../types.js';
import { redactSecrets } from '../utils/redact.js';
import { WARNING_CODES } from '../warnings.js';

/**
* Minimal, dependency-free CVSS v3.0/v3.1 base-score calculator.
Expand Down Expand Up @@ -64,7 +65,7 @@ function invalid(reason: string): CvssParseResult {
);
return {
status: 'invalid',
diagnostic: { code: CVSS_VECTOR_PARSE_FAILED, message },
diagnostic: { code: CVSS_VECTOR_PARSE_FAILED, warnCode: WARNING_CODES.CVSS_UNPARSEABLE, message },
};
}

Expand Down
5 changes: 4 additions & 1 deletion src/core-open/scoring/drift-score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import * as crypto from 'node:crypto';
import type { ProjectScan, DriftScore, Finding, RiskLevel, VibgrateConfig } from '../types.js';
import { licenseParseDiagnostic } from '../licenses/diagnostic.js';
import { WARNING_CODES } from '../warnings.js';
import { normalizeLicenseSourcePath } from '../licenses/dependency-license.js';
import { aggregateDependencyDrift } from './dependency-drift-v3.js';

Expand Down Expand Up @@ -409,7 +410,9 @@ export function generateFindings(
level: 'warning',
message: diag.message,
location,
details: sourcePath ? { raw: diag.raw, sourcePath } : { raw: diag.raw },
details: sourcePath
? { raw: diag.raw, sourcePath, warnCode: WARNING_CODES.LICENSE_UNPARSEABLE }
: { raw: diag.raw, warnCode: WARNING_CODES.LICENSE_UNPARSEABLE },
});
}
findings.push(...licenseFindings);
Expand Down
12 changes: 12 additions & 0 deletions src/core-open/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// ── Core types for Vibgrate CLI ──

import type { DriftBudgetConfig } from './drift-budget.js';
import type { CodedWarning } from './warnings.js';

export type DepSection = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies';

Expand Down Expand Up @@ -477,6 +478,11 @@ export type VulnSeverity = 'low' | 'moderate' | 'high' | 'critical' | 'unknown';
export interface CvssDiagnostic {
/** Stable code, identical for every unparseable vector. */
code: 'cvss-vector-parse-failed';
/**
* Degrade-and-continue warning code. Stable across releases.
* `code` stays `cvss-vector-parse-failed`.
*/
warnCode: 'VG_WARN_CVSS_UNPARSEABLE';
/**
* What failed and what to do next. Deterministic for a given vector.
* Credential-shaped text from the vector is redacted.
Expand Down Expand Up @@ -786,6 +792,12 @@ export interface ScanArtifact {
solutions?: SolutionScan[];
drift: DriftScore;
findings: Finding[];
/**
* Degrade-and-continue notices from this scan (skipped paths, an unreadable
* baseline). Omitted when there are none — absent is not an empty list.
* Sorted by code, then by message. Does not change the exit code.
*/
degradations?: CodedWarning[];
/**
* Repo-relative path (or basename, when the file is outside the repo) of the
* `--baseline` snapshot this scan compared against.
Expand Down
Loading
Loading