Cerbi.Scanner
1.1.0
dotnet tool install --global Cerbi.Scanner --version 1.1.0
dotnet new tool-manifest
dotnet tool install --local Cerbi.Scanner --version 1.1.0
#tool dotnet:?package=Cerbi.Scanner&version=1.1.0
nuke :add-package Cerbi.Scanner --version 1.1.0
Cerbi Scanner
Static logging governance scanner for C#, Go, Java, Node/TypeScript, and Python repositories.
Read-only by default — the scanner never modifies your source code or uploads any data without an explicit opt-in.
Installation
dotnet tool install -g Cerbi.Scanner
Requires the .NET 10 LTS SDK (or .NET 10 runtime for an already-published tool). After install, the cerbi-scanner command is available globally.
To update:
dotnet tool update -g Cerbi.Scanner
Runtime support
Cerbi.Scanner targets net10.0 and is packaged as a framework-dependent .NET global tool. This release does not intentionally change scanner behavior or CLI options, but it does require a .NET 10 LTS runtime on machines that install and run the tool. .NET 8-only hosts must install .NET 10 LTS before running cerbi-scanner.
Breaking change: support for installing/running the global tool on .NET 8-only environments has been removed as part of the move to the current LTS runtime.
Projects
| Project | Type | Purpose |
|---|---|---|
Cerbi.Scanner.Core |
Library | Stable contracts: models, interfaces, rule engine |
Cerbi.Scanner.Cli |
Executable | cerbi-scanner CLI — entry point for local and CI use |
Cerbi.Scanner.DotNet |
Library | Roslyn-based C# logging-call extractor |
Cerbi.Scanner.Go |
Library | Go (zap / zerolog) logging-call extractor |
Cerbi.Scanner.Java |
Library | Java (Log4j2 / SLF4J) logging-call extractor |
Cerbi.Scanner.Node |
Library | JavaScript / TypeScript (Winston / Pino) logging-call extractor |
Cerbi.Scanner.Python |
Library | Python (stdlib logging / structlog) logging-call extractor |
Cerbi.Scanner.Reporting |
Library | JSON and HTML report writers |
Cerbi.Scanner.Sarif |
Library | SARIF 2.1.0 report writer |
Cerbi.Scanner.Telemetry |
Library | Telemetry infrastructure (disabled in v1 — no data is sent) |
Cerbi.Scanner.CerbiShieldClient |
Library | Optional CerbiShield upload client |
CLI Usage
Recommended scanner-first workflow:
mkdir -p scan-results
cerbi-scanner scan \
--path . \
--policy policies/cerbi-policy.yml \
--fail-on error \
--format json --output scan-results/findings.json \
--sarif scan-results/findings.sarif \
--summary scan-results/build-summary.md
Backwards-compatible legacy command:
cerbi-scanner audit .
cerbi-scanner scan [path] [options]
cerbi-scanner audit <path> [options]
Options
| Option | Description |
|---|---|
--path <dir> |
Scan path for scan; defaults to the current directory when omitted |
--policy <file> |
Load a Cerbi YAML policy or JSON governance config (cerbi-policy.yml, cerbi-policy.yaml, cerbi.logging.yml, cerbi_governance.json, or cerbi_governance.generated.json) |
--profile <file> |
Load a legacy Cerbi governance profile JSON file |
--format console\|json\|sarif\|markdown\|html |
Primary output format (default: console) |
--output <file> |
Write report to this file (omit to print JSON to stdout) |
--fail-on info\|warning\|low\|medium\|high\|critical\|warn\|error\|blocker\|none |
Exit 1 if any finding at or above this severity exists; error maps to high, blocker maps to critical, and none disables CI failure |
--sarif <file> |
Also write SARIF output regardless of primary --format |
--summary <file> |
Also write a Markdown CI summary regardless of primary --format |
--generate-profile |
Generate a starter cerbi_governance.json from the scanned codebase |
--profile-output <file> |
Write generated profile to this path |
--generate-policy |
Generate a starter YAML policy; use --output to choose the path |
--exclude <pattern> |
Exclude path patterns (repeatable) |
--no-snippets |
Omit source snippets from findings (privacy mode) |
--config <file> |
Path to cerbi-scanner.json (defaults to cerbi-scanner.json in scan root) |
--upload |
Upload results to CerbiShield — disabled by default, requires CERBI_TOKEN |
Examples
# Scan current directory, print JSON to stdout
cerbi-scanner scan .
# Scan with a governance profile
cerbi-scanner scan ./src --profile cerbi_governance.json
# Write a SARIF report
cerbi-scanner scan ./src --format sarif --output cerbi-scan.sarif
# Fail CI on any critical finding
cerbi-scanner scan ./src --fail-on critical
# Generate a starter profile
cerbi-scanner scan ./src --generate-profile --profile-output cerbi_governance.json
# Use a custom config file
cerbi-scanner scan ./src --config path/to/cerbi-scanner.json
# Upload results (explicit opt-in, requires CERBI_TOKEN env var)
CERBI_TOKEN=<key> cerbi-scanner scan ./src --format json --output out.json --upload
Policy-as-code (preview)
The scanner now supports YAML policy files named cerbi-policy.yml, cerbi-policy.yaml, or cerbi.logging.yml in the scan root, or an explicit --policy <file> path. If no policy file exists, the scanner uses safe built-in defaults plus any existing cerbi-scanner.json configuration.
Supported policy settings today:
disabledRules— disables rule IDs such asCERBI006or legacyCERBI-006.severityOverrides— changes scanner severity for a rule.failThreshold— default CI threshold (warning,error,blocker, ornone; legacy severities still work).allowedStructuredFields— when non-empty, fields outside the allow-list produce a policy finding.disallowedStructuredFields— fields that must not be logged.sensitiveDataPatterns— additional sensitive field names to detect.
Output formats: console, json, sarif, markdown, and existing html. Markdown is designed for Azure DevOps build summaries. SARIF and JSON share the same scanner finding contract as console output.
This repository contains the scanner CLI and docs. No Azure DevOps task-extension source is present in this checkout, so pipeline integration should invoke cerbi-scanner scan directly (or a task wrapper should shell out to it) rather than duplicating detection logic. See cerbi-policy.sample.yml and docs/ci/azure-pipelines-policy-example.yml.
Configuration — cerbi-scanner.json
Place a cerbi-scanner.json in your repository root (or pass --config <path>) to control scanner
behaviour without modifying CI scripts.
{
// Glob patterns excluded from every scan (in addition to --exclude flags)
"defaultExcludes": ["**/obj/**", "**/bin/**", "**/node_modules/**", "**/*.g.cs"],
// Files larger than this are skipped (0 = no limit)
"maxFileSizeKb": 512,
// Abort scan after this many seconds (0 = no timeout)
"scanTimeoutSeconds": 120,
// Equivalent to --fail-on: info | low | medium | high | critical
"failThreshold": "high",
// Per-rule severity overrides (rule ID → new severity)
"severityOverrides": {
"CERBI004": "medium"
},
// Config-based suppressions (inline suppression via comments is a future feature)
"suppressions": [
{
"ruleId": "CERBI001", // required
"filePath": "tests/Fixtures", // optional substring match (/ or \ normalised)
"fieldName": "password", // optional field name match
"reason": "Test fixture — intentional disallowed field",
"expiresOn": "2026-12-31" // optional ISO-8601; suppression ignored after this date
}
]
}
Suppression matching rules
A finding is suppressed when all non-empty fields of a suppression entry match:
| Entry field | Match rule |
|---|---|
ruleId |
Required. Case-insensitive exact match. |
filePath |
Substring match against the finding's file path (separators normalised to /). Omit to match any file. |
fieldName |
Case-insensitive exact match against the finding's AffectedField. Omit to match any field. |
expiresOn |
When present and past today (UTC), the suppression is ignored and a warning is emitted. |
Suppressed findings are excluded from all reports and do not trigger CI failure.
Building
dotnet build Cerbi.Scanner/Cerbi.Scanner.slnx
Running Tests
dotnet test Cerbi.Scanner/Cerbi.Scanner.slnx
Note:
Cerbi.Scanner.Cli.Testsruns the compiledcerbi-scannerbinary as a child process.
Build the solution before running tests so the binary exists.
Core Contracts
Models (Cerbi.Scanner.Core.Model)
| Type | Purpose |
|---|---|
ScanRequest |
Input to the scanner pipeline |
ScanResult |
Output of a full repository scan |
ScanSummary |
Aggregated statistics derived from ScanResult |
ScanFinding |
A single governance violation |
FindingLocation |
Source file + line + column |
FindingSeverity |
Info / Low / Medium / High / Critical |
FindingConfidence |
Low / Medium / High |
LoggerFramework |
Detected logging framework enum |
LoggingCall |
A single extracted logging call |
DiscoveredField |
A field found inside a logging call |
GovernanceProfile |
Rules loaded from a profile file |
ScannerRule |
Serialisable rule descriptor |
RuleEvaluationContext |
Inputs passed to a rule during evaluation |
RuleEvaluationResult |
Output of one rule evaluation |
Interfaces (Cerbi.Scanner.Core.Abstractions)
| Interface | Purpose |
|---|---|
IRepositoryScanner |
Scans an entire repository |
ILanguageScanner |
Scans a single file for one language |
ILoggingCallExtractor |
Extracts logging calls from raw source text |
IScannerRule |
A single evaluable governance rule |
IReportWriter |
Writes a ScanResult to a stream |
IGovernanceProfileLoader |
Loads a GovernanceProfile from a source |
IScanResultUploader |
Uploads results to an external system |
JSON Output Shape
{
"schemaVersion": "1.0",
"scannerVersion": "0.1.0-alpha",
"repositoryPath": "/path/to/repo",
"scannedAt": "2024-01-01T00:00:00+00:00",
"elapsedMs": 42,
"scannedFileCount": 3,
"totalLoggingCallsFound": 12,
"ciThresholdBreached": false,
"findingCount": 2,
"findings": [
{
"ruleId": "CERBI001",
"ruleName": "DisallowedField",
"severity": "High",
"confidence": "High",
"explanation": "Field 'password' is disallowed by the active profile.",
"remediation": "Remove 'password' from the log call.",
"filePath": "/path/to/repo/Service.cs",
"lineNumber": 42,
"column": 12,
"sourceSnippet": "_logger.LogInformation(\"User {password} logged in\", password);",
"loggerFramework": "Mel"
}
]
}
The schemaVersion field is stable. Consumers should read it and reject versions they do not support.
Enterprise Security Posture
What is and is not included in reports
Every report format (JSON, SARIF, HTML, console) is governed by the same data-minimisation rules:
| Data element | JSON | SARIF | HTML | Console | Notes |
|---|---|---|---|---|---|
| Source code | Never | Never | Never | Never | Source snippets are stripped before serialisation. The --no-snippets flag is a belt-and-suspenders guard but is not required. |
| File paths | Yes — local absolute path | Yes | Yes | Yes | Paths reflect the local filesystem at scan time. Normalise/truncate to relative paths before sharing reports externally. |
| Log message templates | Yes | Yes | Yes | Top findings only | Templates contain field names only, never field values from runtime logs. |
| Sensitive literal values | Redacted | Redacted | Redacted | Redacted | All explanation text passes through ReportRedactor before serialisation. See Report redaction. |
| Scan metadata | Yes | Yes | Yes | Summary line | Includes scanner version, UTC timestamp, repo path hash (not the full path), and file count. |
| Telemetry / analytics | Never | Never | Never | Never | No telemetry sink exists in the scanner pipeline. |
| Uploaded data | Never by default | Never by default | Never by default | n/a | Upload requires explicit --upload + CERBI_API_KEY. See Upload opt-in. |
Data collection
| Data | Collected? | Notes |
|---|---|---|
| Source code | Never | Source snippets are omitted from JSON/SARIF output by default (--no-snippets also suppresses them from HTML). |
| File paths | Yes — local paths in findings | Make paths relative before sharing reports externally. |
| Log message templates | Yes — in finding explanations | Templates may contain field names but not field values. |
| Sensitive literal values | Redacted | Explanation text is passed through ReportRedactor before serialisation. See below. |
| Telemetry / analytics | Never | DOTNET_CLI_TELEMETRY_OPTOUT=1 is set in Directory.Build.props; no telemetry sink is wired into the scanner pipeline. |
| Scan results upload | Never by default | Results are only uploaded when --upload is explicitly passed and CERBI_API_KEY is set. |
Report redaction
Before any finding explanation is written to a JSON, SARIF, or HTML report the text is passed through ReportRedactor.Redact() which replaces:
- Password assignments —
password="...",pwd='...',secret="...",token="...", etc. - Connection-string password fields —
Password=abc123;→Password=[REDACTED] - Long Base64-like tokens (≥ 32 chars) after
key=,token=,bearer=, etc.
Redaction is best-effort. Do not treat reports as safe for public sharing without a manual review.
Safe handling of file paths and message templates
The scanner touches the filesystem in three places; each is hardened as follows:
- Scan root — passed on the CLI and resolved to an absolute, normalised path. The scanner never traverses symlinks that resolve outside the scan root.
- Output path — the
--outputtarget is written usingFileStreamwith explicitFileMode.Create. Parent directories are created atomically before the file is opened. The path is not interpreted as a format string or shell command. - Message templates in findings — the raw template string (e.g.
"User {UserId} logged in") is copied verbatim into the findingMessageTemplatefield and then passed throughReportRedactorbefore serialisation. Template strings are neverstring.Format-evaluated by the scanner, so there is no format-injection risk.
Upload opt-in
The --upload flag is an explicit opt-in. It will not transmit any data unless:
--uploadis present on the command line, and- The
CERBI_API_KEYenvironment variable is set.
Omitting either causes the CLI to exit with an error rather than silently skipping the upload.
Telemetry
The scanner has no telemetry of any kind:
DOTNET_CLI_TELEMETRY_OPTOUT=1is declared inDirectory.Build.propsfor all child processes.- No HTTP client, analytics SDK, or background task is wired into the scanner or reporting pipeline.
- The
CerbiShieldUploadClientstub is the only network-capable component; it is a no-op unless explicitly enabled.
Suppression audit trail
When a finding is suppressed via cerbi-scanner.json, the suppression is recorded in the JSON report alongside the finding:
{
"ruleId": "CERBI001",
"suppressed": true,
"suppressionReason": "legacy auth service, tracked in JIRA-123"
}
Suppressions with a past expiresOn date are ignored at scan time and emit a console warning. This ensures suppressions are reviewed periodically rather than silently accumulating forever. The CI workflow treats expired-suppression warnings as advisory (non-blocking) so that teams have time to renew or remove them.
File path safety
- Report output paths are validated and their parent directories are created atomically.
--outputis required when--formatissariforhtml(stdout is JSON-only) to prevent binary content from accidentally polluting shell pipes.- The scanner never follows symlinks outside the scan root.
Binary file protection
Files are checked for null bytes before Roslyn parsing. Any file that appears binary is skipped silently — it will not appear in findings or error output. This prevents false positives from compiled artifacts accidentally included in source trees.
Max file size guard
maxFileSizeKb (default: unlimited) skips files larger than the configured threshold before loading them into memory. Set this to a reasonable value (e.g. 512) in production CI to prevent a single large generated file from causing memory pressure.
Build reproducibility
All projects are built with <Deterministic>true</Deterministic> and <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> (when GITHUB_ACTIONS=true), producing byte-for-byte identical output across machines for the same inputs.
Suppression expiry
Config-based suppressions with a past expiresOn date are ignored and emit a console warning at scan time. This ensures suppressions are reviewed periodically rather than silently accumulating forever.
Current Scope
Supported languages: C# (.NET), Go, Java, Node/TypeScript, Python
Supported frameworks: MEL, Serilog, NLog, log4net, Cerbi, zap, zerolog, Log4j2, SLF4J, Winston, Pino, stdlib logging, structlog
Rules implemented: disallowed fields, sensitive fields, missing required fields
Output formats: JSON (deterministic), SARIF 2.1.0, HTML, console summary
Non-Goals (this release)
- No live connection to the CerbiShield platform (
CerbiShieldClientrequires explicit--upload+ API key). - No Roslyn semantic analysis (syntax-only; no full compilation).
- No IDE extension or editor integration.
- No persistent storage or result history.
- Inline suppression comments (
// cerbi:suppress) — planned for a future release; usecerbi-scanner.jsonsuppressions today.
Architecture Notes
cerbi-scanner audit <path>
└── MultiLanguageRepositoryScanner
├── DotNetRepositoryScanner (.cs — Roslyn syntax tree)
├── GoRepositoryScanner (.go — regex / AST heuristic)
├── JavaRepositoryScanner (.java — regex)
├── NodeRepositoryScanner (.js .ts .mjs .cjs — regex)
└── PythonRepositoryScanner (.py — regex)
└── RuleEngine ← applies IScannerRule[] to each LoggingCall
└── IReportWriter ← JSON | SARIF | HTML | console
└── IScanResultUploader ← no-op unless --upload + CERBI_API_KEY
The Core library has no external dependencies beyond the .NET 10 BCL.
DotNet depends on Microsoft.CodeAnalysis.CSharp (source-only, no compilation).
CI
The included GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request:
- restore —
dotnet restore - build —
dotnet build --no-restore -c Release(warnings-as-errors enabled) - test —
dotnet test --no-build -c Releasewith TRX result publication - pack —
dotnet pack --no-build -c Release - self-scan —
cerbi auditagainsttests/fixtures/SampleDotNetApp(smoke test) - SARIF upload — uploaded to GitHub Code Scanning when
CERBI_SARIF_UPLOAD=trueis set on the default branch
Policy-as-code and automation outputs
Cerbi Scanner now accepts policy files named cerbi-policy.yml, cerbi-policy.yaml, or cerbi.logging.yml in the scan root, or an explicit --policy <file> path. If no policy file is present, the scanner uses safe built-in defaults for sensitive fields, credential-like fields, risky object logging, high-cardinality field names, dynamic/debug-prone logging paths, and exception/object-state exposure.
Supported stable rule IDs are:
| Rule | Purpose |
|---|---|
CERBI001 |
Sensitive data appears in a log message or structured field. |
CERBI002 |
Secret-like token, password, API key, JWT, connection string, or credential appears in a logging path. |
CERBI003 |
Structured log field is not allowed by policy. |
CERBI004 |
High-cardinality field is likely to increase observability cost. |
CERBI005 |
Debug/trace or dynamically constructed logging appears in a production-sensitive path. |
CERBI006 |
Exception or object destructuring may expose sensitive object state. |
Automation-friendly formats are available with --format console|json|sarif|markdown|html. JSON and SARIF preserve the stable finding contract; Markdown is intended for Azure DevOps build summaries. Use --fail-on warning|error|blocker|none (legacy severities such as medium, high, and critical are still accepted) to control CI exit behavior.
Example:
cerbi-scanner audit ./src --policy cerbi-policy.yml --format sarif --output cerbi.sarif --fail-on warning
cerbi-scanner audit ./src --format markdown --output cerbi-summary.md --fail-on none
The scanner can target a solution/repository folder, project folder, ordinary folder, or individual source file. Current language implementations include C#, Go, Java, Node/TypeScript, and Python extractors in this repository; findings use the same language-neutral contract so future scanners can be added without changing downstream automation.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
Targets .NET 10 LTS. Breaking change: .NET 8-only hosts must install .NET 10 LTS before running the global tool.