Detection-as-Code
Manage nano detections as version-controlled YAML and ship them with the nanodac CLI, driven by a coding agent in the OSS build
Detection-as-Code
Detection-as-Code (DaC) brings GitOps practices to detection engineering. Detection rules live as YAML files in a git repository, so you get version control, peer review, automated testing in CI, and reproducible deployments. The nanodac CLI validates those files, tests them against historical data, and syncs them to a nano deployment.
Why Detection-as-Code
UI-based rule editors track changes in audit logs but offer no review gate and no easy rollback. Managing detections as code closes those gaps.
| UI-based editing | Detection-as-Code |
|---|---|
| Changes tracked in audit logs | Full git history with author and rationale |
| No built-in review process | Pull request approvals required |
| Manual deployment | Automated CI/CD pipelines |
| Difficult rollback | git revert recovers instantly |
| Siloed knowledge | Shared repository enables collaboration |
A detection moves through three modes as it earns trust. Rules start silent in staging and graduate to alerting once an analyst has validated them.
| Mode | Purpose | Alert generation |
|---|---|---|
staging | Silent monitoring during development | None |
live | Bake-in period, generates findings | Findings only |
alerting | Full production | Alerts and cases |
The agent angle
The open-source build of nano ships without the built-in pivt AI assistant. The hosted product uses pivt to generate parsers from sample logs, draft detections from natural-language descriptions, and translate plain English into nPL queries. The OSS build leaves that work to you, and a coding agent (Claude Code, Codex, Cursor) fills the gap by reading the reference repositories and authoring the same artifacts.
What you give up, what you get back
| Hosted nano (pivt AI) | OSS nano + coding agent |
|---|---|
| Generate a parser from a sample log paste | Agent reads parsers/ for examples, drafts parser.yaml, you validate with vector vrl |
| Draft a detection from "alert me when…" | Agent reads rules/ for examples, drafts a YAML detection, nanodac validate checks it |
| Convert natural language to nPL | Agent reads the Search Commands docs and drafts the query |
| Auto-tune false positives | Manual review of detection matches, iterate via PR |
| One-click apply from the UI | nanodac sync from CI or a local checkout |
The agent runs locally or in your CI, so log samples and detection logic never leave your environment. That property matters for air-gapped and compliance-bound deployments.
Reference repositories
Three public repos under github.com/nano-rs provide the file shapes, conventions, and examples the agent treats as ground truth.
| Repo | Purpose | Used by |
|---|---|---|
parsers | 60+ sample parsers (parser.yaml with VRL) covering Windows, Sysmon, AWS, GCP, Okta, Palo Alto, CrowdStrike | Parser authoring |
rules | Sample detection rules organized by MITRE tactic, licensed under DRL 1.1 | Detection authoring |
nanodac | Detection-as-code CLI (on npm as @nano-rs/dac-cli) with an MCP server for direct agent integration | Detection authoring and sync |
Supported agents
These docs work for any agent that can read local files and run shell commands. Tested setups:
- Claude Code: Anthropic's CLI; reads
CLAUDE.mdfor repo-specific instructions, supports MCP servers - OpenAI Codex: reads
AGENTS.md, supports MCP servers - Cursor: IDE-native agent; reads
.cursor/rules/andmcp-config.json - Kiro: reads
.kiro/(nanodac ships with a.kiro/directory)
Anywhere these docs reference CLAUDE.md, the equivalent file for your agent works the same way.
nanodac essentials
Install
nanodac is published to npm as @nano-rs/dac-cli — no clone or build:
# Install globally so `nanodac` is on your PATH
npm install -g @nano-rs/dac-cli
# …or run any command without installing
npx @nano-rs/dac-cli <command>Initialize a detections repository:
mkdir my-detections && cd my-detections
git init
nanodac initThis scaffolds nanodac.config.yaml, a detections/ directory with an example rule, and a .github/workflows/detection-sync.yml CI workflow.
Configure API access
Edit nanodac.config.yaml. The API runs on port 3000 and the search service on 3002.
apiUrl: https://nanosiem.example.com:3000
searchUrl: https://nanosiem.example.com:3002
detectionsDir: ./detections
defaults:
severity: medium
mode: stagingSet your API key:
export NANOSIEM_API_KEY=your-api-keyDetection file format
A detection file is YAML frontmatter followed by the nPL query body:
---
title: brute_force_ssh
description: Detects SSH brute force with 10+ failed logins from a single IP
author: security-team
severity: high
mode: staging
detection_mode: scheduled
schedule: "*/5 * * * *"
lookback: 15m
folder: identity
mitre_tactics:
- TA0006
mitre_techniques:
- T1110
tags:
- authentication
- brute_force
ai_triage_hints:
ignore_when:
- "Source IP is a known security scanner"
- "User is a service account with expected auth patterns"
suspicious_when:
- "Multiple usernames targeted from same IP"
- "Activity occurs outside business hours"
context: "SSH brute force can trigger on legitimate password recovery. Check if the user recently requested a password reset."
---
source_type="ssh_logs" event_type="login_failed"
| stats count() as attempts, values(user) as targeted_users, min(timestamp) as first_attempt, max(timestamp) as last_attempt by src_ip
| where attempts > 10
| risk score=70 entity=src_ip factor="SSH brute force"Required fields:
| Field | Description |
|---|---|
title | Snake_case identifier (e.g. brute_force_ssh) |
severity | critical, high, medium, low, or informational |
| query body | nPL query after the frontmatter |
Optional fields:
| Field | Description | Default |
|---|---|---|
description | What the detection identifies | - |
author | Detection author | - |
mode | staging, live, or alerting | staging |
detection_mode | scheduled (only supported value) | scheduled |
schedule | Cron expression | */5 * * * * |
lookback | Time window (15m, 1h, 24h) | 15m |
folder | Grouping folder for the rule (e.g. identity, endpoint, cloud, network) | - |
mitre_tactics | YAML list of MITRE ATT&CK tactic IDs (TA####) | - |
mitre_techniques | YAML list of MITRE ATT&CK technique IDs (T####) | - |
alert_mode | grouped (all matches → one alert) or per_event | grouped |
tags | Array of category tags | [] |
ai_triage_hints | Hints for AI triage and analysts | - |
enabled | Whether the rule is enabled on deploy | true |
Risk is not set in frontmatter. Score an entity inside the query with the risk pipe command:
| risk score=80 entity=user factor="Login from new country"score is a number (or an expression such as if(external_ip, 80, 40)), entity is the field to attribute risk to, and factor is a human-readable reason. A query can apply risk multiple times.
CLI commands
| Command | Purpose |
|---|---|
nanodac validate | Check YAML syntax, schema, required fields, MITRE format, cron and lookback validity. Add --strict to treat warnings as errors. |
nanodac test <file> | Run the detection against historical data and show matches. --hours N / --days N set the window, --all shows every match, --fields a,b limits columns. |
nanodac diff | Show what would change between local files and the remote deployment. --verbose for full output. |
nanodac sync | Push local detections to nano. --dry-run previews, --force skips prompts, --delete-orphans removes remote rules absent locally. |
GitHub Actions CI
nanodac init writes .github/workflows/detection-sync.yml. It validates detections on pull requests and deploys them on push to main:
name: Detection Sync
on:
push:
branches: [main]
paths: ['detections/**']
pull_request:
paths: ['detections/**']
jobs:
validate:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Validate detections
uses: ./.github/actions/nanodac
with:
nanosiem-url: ${{ secrets.NANOSIEM_URL }}
api-key: ${{ secrets.NANOSIEM_API_KEY }}
action: validate
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy detections
uses: ./.github/actions/nanodac
with:
nanosiem-url: ${{ secrets.NANOSIEM_URL }}
api-key: ${{ secrets.NANOSIEM_API_KEY }}
action: deployAdd these repository secrets in GitHub settings:
| Secret | Description |
|---|---|
NANOSIEM_URL | nano API URL (e.g. https://nanosiem.example.com:3000) |
NANOSIEM_API_KEY | API key with detection permissions |
NANOSIEM_SEARCH_URL | Optional search service URL (e.g. https://nanosiem.example.com:3002) |
On a pull request the action posts validation and test results as a PR comment. Merging to main deploys the changed rules; new rules land in staging mode until an analyst promotes them.
Where to go next
- Setup: clone the reference repos, wire up the nanodac MCP server, drop in agent instructions
- Authoring parsers: scaffold a
parser.yamlfrom a log sample and validate it with Vector - Authoring detections: draft a YAML detection, validate it, ship it via
nanodac sync - Crafting searches: turn natural-language hunting questions into nPL queries
For the operations side, see the Investigator agent, which triages alerts and runs investigations against a live deployment.