Skip to content
Open
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
6 changes: 4 additions & 2 deletions cmf-codereview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
`cmf-codereview` is a static analysis CLI for CM Framework (Critical Manufacturing) customization code. It
scans DEE and Orchestration C# code for performance/correctness anti-patterns — loads inside loops,
excessive lazy loading, config misuse, direct entity instantiation, and more — and produces a report
(Markdown or a self-contained HTML page) grouped by severity or by rule. The folders to scan are
(Markdown, a self-contained HTML page, or SARIF 2.1.0). Markdown/HTML group findings by severity or rule.
The folders to scan are
discovered from the repository's own `cmfpackage.json` package manifests, so it works with any
customization project name (`Cmf.Custom.*`, `Cmf.Semi.*`, ...), not a hardcoded layout.

Expand Down Expand Up @@ -75,7 +76,8 @@ baseline (`scan --update-baseline`) so CI gates only fail on new findings. See
as `cmf-dev`/`cmf-pipeline`), wiring `scan`/`report`/`review` under `Commands/`, plus the scanning
engine it calls into: `Analysis/` (syntax-tree analyzers), `Rules/` (the rule engine, including the
reflection-based `EntityCatalogResolver`), `Models/`, `Enums/`, and the report renderers
(`CmReportGenerator.cs` for Markdown, `CmHtmlReportGenerator.cs` for HTML).
(`CmReportGenerator.cs` for Markdown, `CmHtmlReportGenerator.cs` for HTML, and
`CmSarifReportGenerator.cs` for SARIF).
- `tests/` — xUnit tests, referencing `src/cmf-codereview.csproj` directly.
- `npm/` — the npm package (`package.json` with `bin`/`goBinary`, `postinstall.js`, `run.js`, `utils.js`)
that wraps the self-contained per-OS binaries for distribution as a CM CLI plugin.
Expand Down
5 changes: 3 additions & 2 deletions cmf-codereview/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ means a parallel scan and a sequential one produce byte-identical output — par
wall-clock time, never the result. Run with `CODEREVIEW_PROFILE=1` to see each phase's own timing on stderr.

`ReviewCommand` follows the same path and then hands the findings straight to
`ReportCommand.Build`, which renders either Markdown or a self-contained HTML page
(`--report-format`, via `CmReportGenerator` / `CmHtmlReportGenerator`).
`ReportCommand.Build`, which renders Markdown, a self-contained HTML page, or SARIF 2.1.0
(`--report-format`, via `CmReportGenerator` / `CmHtmlReportGenerator` / `CmSarifReportGenerator`).
The scan command can also render findings directly as SARIF while keeping JSON as its default.

## 03 · The rule engine

Expand Down
47 changes: 37 additions & 10 deletions cmf-codereview/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ invalid path, a malformed findings file), or when `scan --fail-on` is used and a

## `scan`

Runs the review engine over a repository and prints the findings as a JSON array. Optionally also saves
that JSON to a file.
Runs the review engine over a repository and prints findings as a JSON array by default, or as a
SARIF 2.1.0 log with `--report-format Sarif`. Optionally also saves the selected output to a file.

```bash
cmf-codereview scan --repository-root <path> [--scope DEEs|Orchestration|Both] [--roots <rel1> <rel2> ...] [--output <file>] [--changed-files-list <file>] [--fail-on Critical|High|Medium] [--update-baseline]
cmf-codereview scan --repository-root <path> [--scope DEEs|Orchestration|Both] [--roots <rel1> <rel2> ...] [--output <file>] [--report-format Json|Sarif] [--changed-files-list <file>] [--fail-on Critical|High|Medium] [--update-baseline]
```

| Option | Required | Default | Description |
|---|---|---|---|
| `--repository-root` | Yes | — | Root of the repository to scan. |
| `--scope` | No | `Both` | Which default folder set to scan: `DEEs`, `Orchestration`, or `Both`. Ignored if `--roots` is given. The concrete folders for `--scope` are auto-detected per repository (see below) — not hardcoded to one project name. |
| `--roots` | No | (derived from `--scope`) | One or more explicit relative folders to scan instead of the `--scope` default. Accepts multiple values, e.g. `--roots Cmf.Custom.Data/Cmf.Custom.Data.Packages/DEEs Cmf.Custom.Business/Cmf.Custom.Common`. |
| `--output` | No | (none — stdout only) | Path to also write the findings JSON to disk. Parent directories are created automatically. |
| `--output` | No | (none — stdout only) | Path to also write findings in the selected format to disk. Parent directories are created automatically. |
| `--report-format` | No | `Json` | `Json` (findings array) or `Sarif` (SARIF 2.1.0). Values are case-insensitive. The selected format applies to stdout and `--output`. |
| `--changed-files-list` | No | (none — reports on every scanned file) | Path to a text file listing changed files, one relative or absolute path per line (e.g. the output of `git diff --name-only`). When given, only findings on those files are returned — but the whole repository is still analyzed for cross-reference and loop-exposure context, so a finding on a changed file caused by an unchanged caller elsewhere is still correctly reported. Intended for CI gates that should only comment on files actually touched by a change. |
| `--fail-on` | No | (none — always exits `0`) | `Critical`, `High`, or `Medium`. When given, the command exits `1` if any reported finding is at or above this severity. Omit it to keep `scan` purely informational. |
| `--update-baseline` | No | `false` | Write all current findings (ignoring any existing baseline) to `.codereview.baseline.json` at the repository root, accepting them as known debt — subsequent scans only report findings not in the baseline. Always exits `0` (ignores `--fail-on`). See [docs/configuration.md](configuration.md). |
Expand Down Expand Up @@ -106,22 +107,22 @@ and `NestedLoopDepth` (set only for the nested-loop reason). The same informatio
## `report`

Renders a report from a findings JSON file previously produced by `scan` (or `review`), in Markdown
(the default) or a self-contained HTML page.
(the default), a self-contained HTML page, or SARIF 2.1.0.

```bash
cmf-codereview report --findings-path <file> [--repository-root <path>] [--report-path <file>] [--report-mode Standard|Detailed] [--report-format Markdown|Html] [--title <text>] [--summary <text>] [--stdout]
cmf-codereview report --findings-path <file> [--repository-root <path>] [--report-path <file>] [--report-mode Standard|Detailed] [--report-format Markdown|Html|Sarif] [--title <text>] [--summary <text>] [--stdout]
```

| Option | Required | Default | Description |
|---|---|---|---|
| `--findings-path` | Yes | — | Path to a findings JSON file produced by `scan`. |
| `--repository-root` | No | current directory | Used only to compute the default `--report-path` when `--report-path` is not given. Ignored when `--stdout` is set. |
| `--report-path` | No | `Tools/CodeReview/Reports/cm-framework-scan-<mode>-<timestamp>.<md\|html>` under `--repository-root` | Output path for the report. The default file extension follows `--report-format`. Ignored when `--stdout` is set. |
| `--report-path` | No | `Tools/CodeReview/Reports/cm-framework-scan-<mode>-<timestamp>.<md\|html\|sarif>` under `--repository-root` | Output path for the report. The default file extension follows `--report-format`. Ignored when `--stdout` is set. |
| `--report-mode` | No | `Standard` | `Standard` (severity-grouped) or `Detailed` (rule-grouped) report shape — see [docs/rules.md](rules.md#cross-reference-and-loop-exposure-context). |
| `--report-format` | No | `Markdown` | `Markdown` (portable — for PR comments, plain viewers) or `Html` (a single self-contained page — no external assets, opens straight from disk — with a live text filter, collapsible severity-colored rule sections, and light/dark theming, for triaging findings in a browser). Both formats honour `--report-mode`. |
| `--report-format` | No | `Markdown` | `Markdown` (portable — for PR comments, plain viewers) or `Html` (a single self-contained page — no external assets, opens straight from disk — with a live text filter, collapsible severity-colored rule sections, and light/dark theming, for triaging findings in a browser). Markdown and HTML honour `--report-mode`; SARIF emits the same log for Standard and Detailed. `Sarif` produces machine-readable static analysis results. |
| `--title` | No | `"C# Code Review for CM Framework Scan"` | Report heading. |
| `--summary` | No | `"Automated CM framework scan for looped loads, smart table resolves, config access, direct entity construction, and related review heuristics."` | Report summary line. |
| `--stdout` | No | `false` | Print the report to stdout instead of writing a file — no file is created when this is set. Works for either format (`--report-format Html` prints the HTML). Useful for piping the content elsewhere (e.g. into a pull request comment) without leaving a report artifact on disk. |
| `--stdout` | No | `false` | Print the report to stdout instead of writing a file — no file is created when this is set. Works for all report formats, including SARIF. Useful for piping the content elsewhere (e.g. into a pull request comment) without leaving a report artifact on disk. |

Prints the resolved report path to stdout on success (or the report content itself, when `--stdout` is set).

Expand Down Expand Up @@ -153,7 +154,7 @@ One-shot: runs `scan` and `report` together against a repository, so both the fi
report are produced from a single command.

```bash
cmf-codereview review --repository-root <path> [--scope DEEs|Orchestration|Both] [--roots <rel1> <rel2> ...] [--findings-path <file>] [--report-path <file>] [--report-mode Standard|Detailed] [--report-format Markdown|Html] [--title <text>] [--summary <text>]
cmf-codereview review --repository-root <path> [--scope DEEs|Orchestration|Both] [--roots <rel1> <rel2> ...] [--findings-path <file>] [--report-path <file>] [--report-mode Standard|Detailed] [--report-format Markdown|Html|Sarif] [--title <text>] [--summary <text>]
```

| Option | Required | Default | Description |
Expand Down Expand Up @@ -192,6 +193,32 @@ Scan and produce a Detailed, self-contained HTML report for browser triage:
cmf-codereview review --repository-root C:\repos\YourProject --report-mode Detailed --report-format Html
```

## SARIF output

Use SARIF 2.1.0 to exchange findings with compatible static analysis viewers:

```powershell
# Scan directly to SARIF. JSON remains the default when --report-format is omitted.
cmf-codereview scan --repository-root . --report-format Sarif --output findings.sarif

# Convert an existing findings JSON file, or scan and create both JSON and SARIF.
cmf-codereview report --findings-path findings.json --report-format Sarif --report-path report.sarif
cmf-codereview review --repository-root . --report-format Sarif --report-path report.sarif
```

Each result includes its rule ID, diagnostic message, suggested fix, and URI-encoded source path.
A source region is included only when the line number is known. Critical/High findings map to
SARIF `error`; Medium maps to `warning`. An empty scan still produces a valid SARIF log.
Standard and Detailed produce identical SARIF; title and summary do not affect SARIF output.
The default report filename uses the `.sarif` extension.

`report --findings-path` accepts the original findings JSON, not a SARIF log. `review` keeps its
intermediate findings file as JSON. Scan filters, suppression, baselines, and `--fail-on` apply
before rendering, so choosing SARIF does not change the findings or exit-code policy.

This functionality writes report files. Publishing results or creating PR annotations requires
a separately configured consumer; no pipeline generation or scaffolding is performed.

## Choosing between the three commands

- Use **`review`** for everyday use — it's the one-command path from repository to report.
Expand Down
97 changes: 97 additions & 0 deletions cmf-codereview/src/CmSarifReportGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Cmf.Cli.Plugin.CodeReview.Models;
using Cmf.Cli.Plugin.CodeReview.Enums;

namespace Cmf.Cli.Plugin.CodeReview;

/// <summary>Renders findings as a SARIF 2.1.0 log for compatible static analysis viewers.</summary>
public static class CmSarifReportGenerator
{
private const string ToolName = "cmf-codereview";
private const string InformationUri = "https://github.com/criticalmanufacturing/cli-plugins/tree/main/cmf-codereview";

private static string SarifLevel(Severity severity) => severity switch
{
Severity.Critical => "error",
Severity.High => "error",
Severity.Medium => "warning",
_ => "note"
};

private static string FormatSarifUri(string path) =>
string.Join("/", path.Replace('\\', '/').Split('/').Select(Uri.EscapeDataString));

/// <summary>Same signature as CmReportGenerator/CmHtmlReportGenerator so it slots into ReportCommand's switch. `title`/`summary` are accepted for signature parity but unused — SARIF has no concept of either.</summary>
public static string BuildStandardReport(IReadOnlyList<Finding> findings, string title, string summary) => Build(findings);

/// <summary>SARIF has no rule-grouped/severity-grouped distinction like Markdown/Html do — Detailed renders identically to Standard.</summary>
public static string BuildDetailedReport(IReadOnlyList<Finding> findings, string title, string summary) => Build(findings);

private static string Build(IReadOnlyList<Finding> findings)
{
var toolVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";

var rules = findings
.Select(f => f.Rule)
.Distinct(StringComparer.Ordinal)
.OrderBy(r => r, StringComparer.Ordinal)
.Select(rule => new JsonObject
{
["id"] = rule,
["shortDescription"] = new JsonObject { ["text"] = rule }
})
.ToArray();

var results = findings.Select(f =>
{
var physicalLocation = new JsonObject
{
["artifactLocation"] = new JsonObject
{
["uri"] = FormatSarifUri(f.RelativePath)
}
};

if (f.LineNumber > 0)
{
physicalLocation["region"] = new JsonObject
{
["startLine"] = f.LineNumber
};
}

return new JsonObject
{
["ruleId"] = f.Rule,
["level"] = SarifLevel(f.Severity),
["message"] = new JsonObject { ["text"] = $"{f.Description} Fix: {f.SuggestedFix}" },
["locations"] = new JsonArray(new JsonObject { ["physicalLocation"] = physicalLocation })
};
}).ToArray();

var sarif = new JsonObject
{
["$schema"] = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
["version"] = "2.1.0",
["runs"] = new JsonArray(
new JsonObject
{
["tool"] = new JsonObject
{
["driver"] = new JsonObject
{
["name"] = ToolName,
["version"] = toolVersion,
["informationUri"] = InformationUri,
["rules"] = new JsonArray(rules.Cast<JsonNode?>().ToArray())
}
},
["results"] = new JsonArray(results.Cast<JsonNode?>().ToArray())
})
};

return sarif.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
}
}
15 changes: 11 additions & 4 deletions cmf-codereview/src/Commands/ReportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

namespace Cmf.Cli.Plugin.CodeReview.Commands;

/// <summary>cmf-codereview report — renders a Markdown report from a findings JSON file.</summary>
[CmfCommand("report", Description = "Render a Markdown report from a findings JSON file.")]
/// <summary>cmf-codereview report — renders a Markdown, HTML, or SARIF report from findings JSON.</summary>
[CmfCommand("report", Description = "Render a Markdown, HTML, or SARIF report from a findings JSON file.")]
public class ReportCommand : BaseCommand
{
public const string DefaultTitle = "C# Code Review for CM Framework Scan";
Expand All @@ -23,7 +23,7 @@ public override void Configure(Command cmd)
var repositoryRootOption = new Option<string?>("--repository-root", "Repository root, used only to default --report-path");
var reportPathOption = new Option<string?>("--report-path", "Output path for the report; defaults to Tools/CodeReview/Reports/cm-framework-scan-<mode>-<timestamp>.md under --repository-root");
var reportModeOption = new Option<ReportMode>("--report-mode", () => ReportMode.Standard, "Standard (severity-grouped) or Detailed (rule backlog) report shape");
var reportFormatOption = new Option<ReportFormat>("--report-format", () => ReportFormat.Markdown, "Markdown (portable, for PR comments) or Html (self-contained, filterable page for browser triage)");
var reportFormatOption = new Option<ReportFormat>("--report-format", () => ReportFormat.Markdown, "Markdown (portable, for PR comments), Html (self-contained browser report), or Sarif (SARIF 2.1.0 log)");
var titleOption = new Option<string>("--title", () => DefaultTitle, "Report title");
var summaryOption = new Option<string>("--summary", () => DefaultSummary, "Report summary line");
var stdoutOption = new Option<bool>("--stdout", "Print the report to stdout instead of writing a file (e.g. for piping into a PR comment). No file is created when this is set.");
Expand Down Expand Up @@ -67,8 +67,10 @@ public static string BuildContent(string findingsPath, ReportMode reportMode, Re
return (reportMode, reportFormat) switch
{
(ReportMode.Detailed, ReportFormat.Html) => CmHtmlReportGenerator.BuildDetailedReport(findings, title, summary),
(ReportMode.Detailed, ReportFormat.Sarif) => CmSarifReportGenerator.BuildDetailedReport(findings, title, summary),
(ReportMode.Detailed, _) => CmReportGenerator.BuildDetailedReport(findings, title, summary),
(_, ReportFormat.Html) => CmHtmlReportGenerator.BuildStandardReport(findings, title, summary),
(_, ReportFormat.Sarif) => CmSarifReportGenerator.BuildStandardReport(findings, title, summary),
_ => CmReportGenerator.BuildStandardReport(findings, title, summary)
};
}
Expand All @@ -84,7 +86,12 @@ public static string Build(string findingsPath, string? repositoryRoot, string?
var reportDirectory = CodeReviewPaths.GetDefaultReportsDirectory(root);
Directory.CreateDirectory(reportDirectory);
var suffix = reportMode == ReportMode.Detailed ? "detailed" : "standard";
var extension = reportFormat == ReportFormat.Html ? "html" : "md";
var extension = reportFormat switch
{
ReportFormat.Html => "html",
ReportFormat.Sarif => "sarif",
_ => "md"
};
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
reportPath = Path.Combine(reportDirectory, $"cm-framework-scan-{suffix}-{timestamp}.{extension}");
}
Expand Down
Loading
Loading