Cerbi.Scanner 1.1.0

dotnet tool install --global Cerbi.Scanner --version 1.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Cerbi.Scanner --version 1.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#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 as CERBI006 or legacy CERBI-006.
  • severityOverrides — changes scanner severity for a rule.
  • failThreshold — default CI threshold (warning, error, blocker, or none; 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.Tests runs the compiled cerbi-scanner binary 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 assignmentspassword="...", pwd='...', secret="...", token="...", etc.
  • Connection-string password fieldsPassword=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:

  1. Scan root — passed on the CLI and resolved to an absolute, normalised path. The scanner never traverses symlinks that resolve outside the scan root.
  2. Output path — the --output target is written using FileStream with explicit FileMode.Create. Parent directories are created atomically before the file is opened. The path is not interpreted as a format string or shell command.
  3. Message templates in findings — the raw template string (e.g. "User {UserId} logged in") is copied verbatim into the finding MessageTemplate field and then passed through ReportRedactor before serialisation. Template strings are never string.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:

  1. --upload is present on the command line, and
  2. The CERBI_API_KEY environment 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=1 is declared in Directory.Build.props for all child processes.
  • No HTTP client, analytics SDK, or background task is wired into the scanner or reporting pipeline.
  • The CerbiShieldUploadClient stub 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.
  • --output is required when --format is sarif or html (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 (CerbiShieldClient requires 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; use cerbi-scanner.json suppressions 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:

  1. restoredotnet restore
  2. builddotnet build --no-restore -c Release (warnings-as-errors enabled)
  3. testdotnet test --no-build -c Release with TRX result publication
  4. packdotnet pack --no-build -c Release
  5. self-scancerbi audit against tests/fixtures/SampleDotNetApp (smoke test)
  6. SARIF upload — uploaded to GitHub Code Scanning when CERBI_SARIF_UPLOAD=true is 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
1.1.0 119 7/2/2026
1.0.0 132 6/2/2026

Targets .NET 10 LTS. Breaking change: .NET 8-only hosts must install .NET 10 LTS before running the global tool.