Detection-as-Code
Detection-as-Code
Detection-as-Code (DaC) brings GitOps practices to security detection engineering. By managing detection rules as code in git repositories, teams gain version control, peer review, automated testing, and reproducible deployments.
Why Detection-as-Code?
Traditional detection management through UI-based editors creates challenges:
| Traditional Approach | Detection-as-Code |
|---|---|
| Changes tracked in audit logs | Full git history with context |
| 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 |
Benefits:
- Version Control - Every detection change is tracked with author, timestamp, and rationale
- Peer Review - Security team reviews PRs before rules go live
- Testing - Validate rules against historical data in CI
- Rollback - Revert problematic changes in seconds
- Collaboration - Detection logic is visible and shareable
Getting Started
Install nanodac CLI
nanodac is available as a separate repository: github.com/the2dl/nanodac
# Clone the repo
git clone https://github.com/the2dl/nanodac.git
cd nanodac
# Install dependencies and build
npm install
npm run build
# Run commands via npx
npx nanodac <command>Initialize a Repository
mkdir my-detections && cd my-detections
git init
npx nanodac initThis creates:
my-detections/
├── nanodac.config.yaml
├── detections/
│ └── examples/
│ └── brute_force_ssh.yaml
└── .github/
└── workflows/
└── detection-sync.ymlConfigure API Access
Edit nanodac.config.yaml:
apiUrl: https://nanosiem.example.com:3001
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
Detection files use YAML frontmatter followed by the query body:
---
title: brute_force_ssh
description: Detects SSH brute force attempts with 10+ failed logins from single IP
author: security-team
severity: high
mode: staging
detection_mode: scheduled
schedule: "*/5 * * * *"
lookback: 15m
mitre_tactics: TA0006
mitre_techniques: T1110
risk_score: auto
risk_entity: src_ip
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 user recently requested password reset."
---
source_type=ssh_logs
| where action="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 > 10Required Fields
| Field | Description |
|---|---|
title | Snake_case identifier (e.g., brute_force_ssh) |
severity | critical, high, medium, low, or informational |
query | nano 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 or real-time | scheduled |
schedule | Cron expression | */5 * * * * |
lookback | Time window (15m, 1h, 24h) | 15m |
mitre_tactics | MITRE ATT&CK tactic IDs | - |
mitre_techniques | MITRE ATT&CK technique IDs | - |
risk_score | 0-100 or auto | auto |
risk_entity | Field for risk scoring | auto-detect |
tags | Array of category tags | [] |
ai_triage_hints | Hints for AI triage and analysts | - |
enabled | Whether rule is enabled on deploy | true |
alert_mode | grouped or per_event — how matches map to alerts | grouped |
CLI Commands
Validate Detections
Check detection files for errors before deploying:
# Validate all detections
nanodac validate
# Validate specific files
nanodac validate detections/malware/*.yaml
# Strict mode - treat warnings as errors
nanodac validate --strictValidation checks:
- YAML syntax and schema compliance
- Required fields present
- MITRE ATT&CK format (TA####, T####)
- Cron expression validity
- Lookback period format
Test Against Historical Data
See what a detection would match before deploying:
# Test with 7-day lookback (default)
nanodac test detections/brute_force_ssh.yaml
# Test last 24 hours
nanodac test detections/brute_force_ssh.yaml --hours 24
# Show all matching events
nanodac test detections/brute_force_ssh.yaml --all
# Show specific fields only
nanodac test detections/brute_force_ssh.yaml --fields src_ip,user,attempts
# Compact output
nanodac test detections/brute_force_ssh.yaml --compactCompare Local vs Remote
See what would change before deploying:
# Summary of differences
nanodac diff
# Detailed diff output
nanodac diff --verboseDeploy Detections
Sync local detection files to nano:
# Preview changes (dry-run)
nanodac sync --dry-run
# Deploy changes
nanodac sync
# Skip confirmation prompts
nanodac sync --force
# Delete remote rules not in local files
nanodac sync --delete-orphansRecommended Workflow
1. Create a Feature Branch
git checkout -b detect/lateral-movement-psexec2. Write the Detection
Create detections/lateral_movement/psexec_execution.yaml:
---
title: psexec_remote_execution
description: Detects PsExec lateral movement to remote hosts
author: jane.doe@company.com
severity: high
mode: staging
detection_mode: scheduled
schedule: "*/5 * * * *"
lookback: 15m
mitre_tactics: TA0008
mitre_techniques: T1021.002
risk_score: 75
risk_entity: dest_host
tags:
- lateral_movement
- psexec
ai_triage_hints:
ignore_when:
- "Destination is a jump server for admin access"
- "User is in approved admin group with documented access"
- "Activity matches known deployment automation"
suspicious_when:
- "User has never accessed this host before"
- "Destination host is sensitive (DC, file server)"
- "Activity occurs outside business hours"
context: "PsExec is legitimate admin tool but commonly used by attackers. Verify user had business need for remote access."
---
source_type=windows_security
| where event_id=4688
| where process_name CONTAINS "psexec"
| stats count() as executions,
values(dest_host) as targets,
min(timestamp) as first_seen,
max(timestamp) as last_seen
by src_host, user
| where count(targets) > 13. Validate and Test Locally
# Check for errors
nanodac validate detections/lateral_movement/psexec_execution.yaml
# Test against last 14 days
nanodac test detections/lateral_movement/psexec_execution.yaml --days 14Review the output - check for:
- Excessive matches (possible false positives)
- No matches (query might be too narrow)
- Expected behavior for known activity
4. Create Pull Request
git add detections/lateral_movement/psexec_execution.yaml
git commit -m "feat(detection): add PsExec lateral movement detection
Detects PsExec usage to multiple remote hosts from single source.
Maps to MITRE T1021.002 (Remote Services: SMB/Windows Admin Shares).
Tested against 14 days of data: 3 matches, all expected admin activity."
git push -u origin detect/lateral-movement-psexec5. CI Validates Automatically
GitHub Actions runs on the PR:
- Validates YAML syntax and schema
- Tests query against nano API
- Posts results as PR comment
6. Team Reviews
Reviewers check:
- Detection logic correctness
- MITRE mapping accuracy
- Threshold appropriateness
- Triage hint completeness
- Query performance
7. Merge Deploys to Production
After approval, merge to main:
- Detection automatically deploys to nano
- Starts in
stagingmode for silent monitoring - Analyst promotes to
alertingafter validation
GitHub Actions Setup
Basic Workflow
Create .github/workflows/detection-sync.yml:
name: Detection Sync
on:
push:
branches: [main]
paths: ['detections/**']
pull_request:
paths: ['detections/**']
jobs:
validate:
name: Validate Detections
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:
name: Deploy Detections
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: deployRepository Secrets
Add these secrets in GitHub repository settings:
| Secret | Description |
|---|---|
NANOSIEM_URL | nano API URL (e.g., https://nanosiem.example.com:3001) |
NANOSIEM_API_KEY | API key with detection permissions |
NANOSIEM_SEARCH_URL | Optional search service URL |
PR Comment Example
When a PR is opened, the action posts validation results:
## Detection Validation Results
| Status | Count |
|--------|-------|
| Valid | 5 |
| Warnings | 1 |
| Errors | 0 |
### Warnings
**detections/risky_rule.yaml**
- mitre: No MITRE ATT&CK mappings specified
### Test Results
| Detection | Matches (7d) | Status |
|-----------|--------------|--------|
| brute_force_ssh | 12 | OK |
| psexec_execution | 3 | OK |AI-Assisted Detection Writing
MCP Server Setup
The nanodac MCP server enables AI assistants to create and manage detections.
For Cursor, add to .cursor/mcp.json:
{
"mcpServers": {
"nanodac": {
"command": "node",
"args": ["/Users/you/nanodac/packages/mcp-server/dist/index.js"],
"env": {
"NANOSIEM_API_URL": "https://nanosiem.example.com:3001",
"NANOSIEM_SEARCH_URL": "https://nanosiem.example.com:3002",
"NANOSIEM_API_KEY": "your-api-key"
}
}
}
}For Kiro, add to .kiro/settings/mcp.json:
{
"mcpServers": {
"nanodac": {
"command": "node",
"args": ["/Users/you/nanodac/packages/mcp-server/dist/index.js"]
}
}
}Replace /Users/you/nanodac with your actual nanodac installation path.
AI Tools Available
| Tool | Description |
|---|---|
list_detections | List all detections |
get_detection | Get detection by ID/name |
create_detection | Create from YAML content |
validate_detection | Validate syntax locally |
test_detection | Test against historical data |
search | Execute ad-hoc queries |
get_schema | Get file format documentation |
create_issue | Create a GitHub issue |
list_issues | List GitHub issues |
create_branch | Create feature branch |
write_detection_file | Write detection to disk |
commit_and_push | Commit and push changes |
create_pull_request | Create PR with issue linking |
AI-Driven Detection Workflow
The most powerful feature is the new_detection_workflow prompt. Just tell the AI:
"Let's create a new detection for PowerShell encoded commands"
The AI will automatically:
- Create a GitHub issue to track the work
- Create a feature branch (
detect/powershell-encoded-commands) - Generate the detection with MITRE mappings and triage hints
- Write the file to
detections/ - Validate and test against historical data
- STOP and show you the results
After you review and approve, the AI will:
7. Commit and push the changes
8. Create a PR linked to the issue (Closes #X)
9. Add "awaiting-review" label
The issue closes automatically when the PR is merged.
Example AI Prompts
Generate a detection:
"Create a detection for PowerShell execution with encoded commands"
Tune a noisy rule:
"The brute_force_ssh detection is triggering on security scanners. Add an exclusion for our Nessus scanner at 10.1.1.50"
Explain a detection:
"Explain what the lateral_movement_wmi detection does and what MITRE techniques it covers"
Issue Tracking Integration
Track detection improvements with GitHub issues:
# Create an issue for detection tuning
nanodac issue create \
--title "Tune brute_force_ssh - false positives from scanners" \
--body "Detection triggering on security scanners. Need to add exclusions." \
--labels detection,tuning,false-positive
# List open detection issues
nanodac issue list --labels detectionRequires GitHub CLI (gh) to be installed and authenticated.
Best Practices
Directory Organization
detections/
├── authentication/
│ ├── brute_force_ssh.yaml
│ ├── failed_mfa.yaml
│ └── impossible_travel.yaml
├── lateral_movement/
│ ├── psexec_execution.yaml
│ └── wmi_process_creation.yaml
├── malware/
│ ├── known_bad_hashes.yaml
│ └── suspicious_powershell.yaml
└── data_exfiltration/
├── large_upload.yaml
└── cloud_storage_access.yamlNaming Conventions
- Snake_case for titles:
brute_force_ssh,lateral_movement_psexec - Descriptive names that indicate the threat, not the query
- Prefix experimental rules:
wip_,test_
Mode Progression
| Mode | Purpose | Alert Generation |
|---|---|---|
staging | Silent monitoring during development | No |
live | Bake-in period, generates findings | Findings only |
alerting | Full production | Alerts + Cases |
Commit Messages
Use conventional commits for clear history:
feat(detection): add PsExec lateral movement detection
fix(detection): reduce brute_force_ssh false positives
tune(detection): adjust dns_tunnel threshold from 100 to 500
docs(detection): add triage hints to suspicious_powershellTesting Guidelines
Before deploying, always verify:
- Match count - Excessive matches indicate false positives
- Sample review - Inspect actual matched events
- Performance - Complex queries may impact system load
- Coverage - Ensure query catches intended behavior
# Check match volume over 30 days
nanodac test detections/new_rule.yaml --days 30 --all | wc -l
# Review samples
nanodac test detections/new_rule.yaml --limit 10AI Triage Hints
Always include triage hints to help analysts and AI systems:
ai_triage_hints:
ignore_when:
- "Source is known security tool (scanner, EDR)"
- "Activity matches documented automation"
- "User is in approved exception list"
suspicious_when:
- "First time user accessed this system"
- "Activity outside business hours"
- "Multiple systems affected in short timeframe"
context: "Background information helping analysts understand what to investigate."Troubleshooting
Validation Fails
YAML syntax error:
Error: YAML parse error at line 15Fix: Check indentation and YAML formatting.
Missing required field:
Error: title is requiredFix: Add the missing field to frontmatter.
Invalid MITRE format:
Warning: mitre_tactics should be TA#### formatFix: Use TA0001 format for tactics, T1059 for techniques.
Test Command Fails
API key invalid:
Error: Invalid API keyFix: Check NANOSIEM_API_KEY environment variable.
Query syntax error:
Error: Query parse error at position 42Fix: Validate query syntax in nano Search first.
Sync Conflicts
Detection already exists:
Warning: Detection 'brute_force_ssh' already exists with different IDOptions:
- Use
--forceto overwrite - Delete the existing rule first
- Rename your detection
Next Steps
- Detection Rules Reference - Query language and rule types
- Risk-Based Alerting - Risk scoring configuration
- pivt AI Settings - AI-assisted detection configuration
- Case Management - Track detection issues