Appearance
Security Agent Development Guide
Overview
This guide covers how to build, test, and deploy a new security monitoring agent. Agents are Python modules that run inside the bedrock-hub Container Apps Job every 4–6 hours. Each agent checks one domain (identity, network, a kill chain stage, etc.), posts findings to the SecOps platform, and returns a structured result dict.
All agent code lives in bedrock-hub/agents/. The orchestrator picks them up automatically once registered.
Architecture Recap
orchestrator.py
└── runs mini-agents in parallel (ThreadPoolExecutor)
│ POST /api/incidents per finding
▼
analyst_agent.py — rules engine: NEW → CLOSED (known-good) or OPEN
engineer_agent.py — recurring noise suppression, no LLM
history_matcher_agent.py — pattern matching against resolved history
detection_agent.py — LLM enrichment of remaining OPEN incidents
triage_agent.py / security_agent.py — pages Rory if warrantedYour new agent is a mini-agent — it runs in the parallel pool, posts findings, and returns. It does not run the analyst pipeline; that happens after all mini-agents complete.
Agent File Structure
Create one file: bedrock-hub/agents/<domain>_agent.py
Naming convention: <domain>_<environment>_agent.py for environment-specific agents, <domain>_agent.py for PROD-only or cross-environment.
Examples: lateral_movement_agent.py, fim_dde_agent.py, aws_persistence_agent.py
Required Output Contract
Every agent's run() function must return this exact shape:
python
{
"findings": [ # list of finding dicts — empty list if nothing found
{
"severity": "critical|high|medium|low",
"resource": "short-identifier", # VM name, account, IP, etc.
"description": "what happened", # 1–3 sentences, specific
"evidence": "raw supporting data", # log line, KQL result, API response
"hipaa_risk": "HIPAA context", # which safeguard is implicated
"recommended_action": "what to do", # specific steps
# Optional but encouraged:
"mitre_technique": "T1003",
"mitre_tactic": "Credential Access",
"source_system": "PROD", # PROD | DDE | AWS
}
],
"iocs": ["list-of-ioc-strings"], # same as finding resources
"confidence": 90, # 0–100 int
"summary": "One paragraph describing what was checked and what was found.",
"immediate_action_required": False, # True only for active breaches
}Never raise an exception from run() — catch all exceptions, log them, and return a degraded result with confidence: 0 and an explanatory summary. An uncaught exception kills the agent thread and produces no findings.
Boilerplate Template
python
"""
<domain>_agent.py — <one line description>
Checks: <what this agent monitors>
Data source: <LAW query | API | etc.>
Fires on: <what triggers a finding>
MITRE: <ATT&CK technique>
"""
import logging
import os
import secops_client
log = logging.getLogger(__name__)
# Declare what this agent checks — used for audit trail and evidence registration.
AGENT_QUERIES = [
{
"description": "One-line description of what this query detects",
"mitre_technique": "T1234",
"mitre_tactic": "Tactic Name",
"source_system": "PROD", # PROD | DDE | AWS
},
]
def run() -> dict:
secops_client.register_queries(AGENT_QUERIES, "<domain>")
log.info("Starting <domain>_agent...")
findings: list[dict] = []
try:
# ── Your detection logic here ─────────────────────────────────────
results = _check_something()
for row in results:
if _is_suspicious(row):
findings.append({
"severity": "high",
"resource": row["resource_name"],
"description": f"Suspicious activity detected on {row['resource_name']}: ...",
"evidence": str(row),
"hipaa_risk": "HIPAA §164.312 — ...",
"recommended_action": "Investigate and contain.",
"mitre_technique": "T1234",
"mitre_tactic": "Tactic Name",
"source_system": "PROD",
})
except Exception as exc:
log.exception("Unhandled error in <domain>_agent: %s", exc)
return {
"findings": [],
"iocs": [],
"confidence": 0,
"summary": f"<domain>_agent failed with error: {exc}. Check logs.",
"immediate_action_required": False,
}
iocs = [f["resource"] for f in findings]
if not findings:
summary = "No suspicious <domain> activity detected. <N> events checked."
else:
summary = f"{len(findings)} finding(s): {', '.join(iocs)}. See findings for details."
return {
"findings": findings,
"iocs": iocs,
"confidence": 90,
"summary": summary,
"immediate_action_required": any(f["severity"] == "critical" for f in findings),
}
def _check_something() -> list[dict]:
"""Your detection logic — LAW query, API call, etc."""
raise NotImplementedErrorQuerying Log Analytics (KQL)
Most PROD and DDE agents query the central LAW workspace. Use kql_utils.py:
python
from kql_utils import run_kql_query
# Workspace IDs are loaded from env — don't hardcode.
# LAW_WORKSPACE_ID → cirius-logging-law-central (PROD, Sentinel-enabled)
# DDE_LAW_WORKSPACE_ID → ciriusdde-logging-law-central (DDE)
results = run_kql_query("""
SecurityEvent
| where TimeGenerated > ago(4h)
| where EventID == 4688
| where NewProcessName has_any ("mimikatz", "procdump", "lsass")
| project TimeGenerated, Computer, Account, NewProcessName, CommandLine
""")
# results is a list of dicts — one per rowCritical LAW gotcha: SecurityEvent (EventID 4688, 4624, etc.) only exists in the Sentinel-enabled workspace (LAW_WORKSPACE_ID). If you query the ops workspace for SecurityEvent you get empty results silently. Always query LAW_WORKSPACE_ID for Windows security events. See Key Learnings for details.
Hostname normalization: LAW mixes FQDNs and short names. Normalize with:
kql
| extend Host = toupper(tostring(split(Computer, ".")[0]))Querying AWS (CloudTrail / Security Hub)
For AWS agents, use boto3 with the credentials provided via the Container App's managed identity. Example:
python
import boto3
def _get_cloudtrail_events(account_id: str) -> list[dict]:
client = boto3.client("cloudtrail", region_name="us-west-2")
resp = client.lookup_events(
LookupAttributes=[{"AttributeKey": "EventName", "AttributeValue": "ConsoleLogin"}],
MaxResults=50,
)
return resp.get("Events", [])AWS agents typically iterate all 7 account IDs. See aws_cloudtrail_agent.py for the cross-account assume-role pattern.
Severity Guide
| Severity | When to use |
|---|---|
critical | Active breach indicators — log cleared (1102), LSASS access, break-glass login, ransomware behavioral pattern. Pages Rory immediately. |
high | Strong indicators requiring same-day response — new persistence outside CM window, spray pattern, admin share access from unexpected source. |
medium | Anomalies requiring investigation but not urgent — compliance drift, cert expiring >30 days, unusual but possibly-legitimate behavior. |
low | Informational — policy drift, minor hygiene issues. Review in weekly security review. |
When in doubt, go high. A false-positive CRITICAL is annoying but safe. A missed CRITICAL is a ransomware incident.
Registering Your Agent in the Orchestrator
Once the agent file exists, add it to two places:
1. agents/run_agent.py — AGENTS list
python
AGENTS = [
...
("lateral_movement", "lateral_movement_agent"), # ← add here
...
]The string key (first element) is used for python run_agent.py lateral_movement.
2. agents/orchestrator.py (or the appropriate mini_orchestrator)
The main orchestrator (orchestrator.py) handles PROD agents. mini_orchestrator_dde.py handles DDE. mini_orchestrator_aws.py handles AWS.
In the right orchestrator, import your module and add it to the parallel pool:
python
import lateral_movement_agent # ← add import at top
# In run_mini_agents():
futures = {
executor.submit(lateral_movement_agent.run): "lateral_movement",
...
}Testing Locally
bash
cd bedrock-hub/agents
# Set required env vars (pull from Key Vault or use test values)
export LAW_WORKSPACE_ID=5d76d1f2-...
export SECOPS_API_URL=https://secops.bedrockcybersecurity.org
export INGEST_API_KEY=...
# Run your agent standalone
python run_agent.py lateral_movement
# Or run it directly
python lateral_movement_agent.pyThe if __name__ == "__main__": block at the bottom of each agent prints the result as JSON. Add one to your agent:
python
if __name__ == "__main__":
import json, sys
result = run()
print(json.dumps(result, indent=2, default=str))
if result.get("immediate_action_required"):
sys.exit(1)Check the output carefully:
findingsis a list (neverNone)confidenceis 0–100 (not a string)summarydescribes what was actually checked, not just a generic message- No unhandled exceptions
Deployment
Agents deploy via the bedrock-hub GitHub Actions pipeline on merge to main. Never deploy locally.
- Create a branch, write the agent, register it, open a PR
- CI runs linting and basic import checks
- Rory reviews and merges
- GitHub Actions builds a new Docker image, pushes to
ciriusagentsprod.azurecr.io, and updates the Container Apps Job to use the new image revision - The next scheduled run (within 6 hours) picks up the new agent automatically
To force a run without waiting for the schedule, trigger the Container Apps Job manually from the Azure Portal: rg-logging-logs → job-orchestrator-cirius → Run.
Kill Chain Agent Specs (Reference)
The six kill chain agents scheduled for Phase 2 build. Each fires CRITICAL with no analyst gate delay — direct page.
| Agent file | Kill chain stage | Key events | Fires on |
|---|---|---|---|
execution_agent.py | Execution | EventID 4688, 4104 | LOLBins, mimikatz, mshta, wscript spawning from unexpected parents; processes in temp/AppData |
persistence_agent.py | Persistence | EventID 7045, 4698, FIM | New service or scheduled task outside a CM window |
credential_dumping_agent.py | Credential Dump | EventID 4688 (LSASS), multi-target auth | LSASS process access; spray pattern (5+ failed auth then success across 3+ machines) |
lateral_movement_agent.py | Lateral Movement | EventID 4624, 4648, 5140 | Same account on 3+ machines in 30 min; admin shares (C$, ADMIN$); explicit creds workstation→server |
exfiltration_agent.py | Exfiltration | Palo Alto CEF, App-ID, DNS Security | Outbound bytes above baseline; new destination not seen in 30 days; App-ID: rclone/WinSCP/mega |
defense_evasion_agent.py | Defense Evasion | EventID 1102, Cortex silence | Log cleared = CRITICAL immediately; Cortex returning 0 findings while other agents elevated = HIGH |
All six should set immediate_action_required: True for CRITICAL findings.
Common Mistakes
- Returning
Noneinstead of an empty findings list — breaks the orchestrator - Hardcoding workspace IDs — always use environment variables from Key Vault
- Querying the ops workspace for SecurityEvent — returns empty silently, see Key Learnings
- Raising exceptions from
run()— kills the thread, produces no findings, no alert - Setting
immediate_action_required: Truefor medium findings — pages Rory unnecessarily - Not stamping
source_system— incidents from different environments collide on deduplication
Related Documents
- Security Monitoring Architecture — full system overview
- SecOps Platform Guide — platform API reference
- Kill Chain Coverage — what's detected vs. what has gaps
- Key Learnings — LAW/KQL gotchas
Document History
| Date | Change | Author |
|---|---|---|
| May 2026 | Initial draft — based on code review of bedrock-hub/agents/, cert_expiry_agent.py as reference implementation, and CLAUDE.md kill chain specs | Rory |