Cerbi.Scanner
1.0.0
See the version list below for details.
dotnet tool install --global Cerbi.Scanner --version 1.0.0
dotnet new tool-manifest
dotnet tool install --local Cerbi.Scanner --version 1.0.0
#tool dotnet:?package=Cerbi.Scanner&version=1.0.0
nuke :add-package Cerbi.Scanner --version 1.0.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 .NET 8 SDK or later. After install, the cerbi-scanner command is available globally.
To update:
dotnet tool update -g Cerbi.Scanner
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
cerbi audit <path> [options]
Options
| Option | Description |
|---|---|
--profile <file> |
Load a Cerbi governance profile JSON file |
--format json\|sarif\|html |
Output format (default: json) |
--output <file> |
Write report to this file (omit to print JSON to stdout) |
--fail-on info\|low\|medium\|high\|critical |
Exit 1 if any finding at or above this severity exists |
--generate-profile |
Generate a starter cerbi_governance.json from the scanned codebase |
--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_API_KEY |
Examples
# Scan current directory, print JSON to stdout
cerbi audit .
# Scan with a governance profile
cerbi audit ./src --profile cerbi_governance.json
# Write a SARIF report
cerbi audit ./src --format sarif --output cerbi-scan.sarif
# Fail CI on any critical finding
cerbi audit ./src --fail-on critical
# Generate a starter profile
cerbi audit ./src --generate-profile --output cerbi_governance.json
# Use a custom config file
cerbi audit ./src --config path/to/cerbi-scanner.json
# Upload results (explicit opt-in, requires CERBI_API_KEY env var)
CERBI_API_KEY=<key> cerbi audit ./src --format json --output out.json --upload
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": {
"CERBI-004": "medium"
},
// Config-based suppressions (inline suppression via comments is a future feature)
"suppressions": [
{
"ruleId": "CERBI-001", // 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": "CERBI-001",
"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": "CERBI-001",
"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 8 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
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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.