Skip to content

feat: add constrained red attack runner#24

Open
Ker102 wants to merge 2 commits into
mainfrom
feature/red-agent-runner
Open

feat: add constrained red attack runner#24
Ker102 wants to merge 2 commits into
mainfrom
feature/red-agent-runner

Conversation

@Ker102
Copy link
Copy Markdown
Owner

@Ker102 Ker102 commented May 10, 2026

Summary

  • add a constrained red attack runner that only executes generated run-directory attack.py scripts
  • record command, stdout, stderr, return code, target URL, stage, and timestamps into events.jsonl
  • execute the allowlisted attack command before and after remediation
  • update generated attack scripts and docs to describe the command boundary

Tests

  • python -m unittest discover -s tests -v
  • python -m ruff check src tests
  • python -m mypy src
  • python -m nullstate run examples/aws-public-s3 --offline --mock-agents --runs-dir runs/red-tool-smoke-2

Summary by CodeRabbit

Release Notes

  • Documentation

    • Expanded purple-teaming loop workflow with step-by-step attack and remediation steps
    • Enhanced security model with specifics on attack execution constraints and evidence capture
    • Updated demo script to show event logs and execution metadata
  • New Features

    • Constrained attack script execution with validation ensuring only allowlisted scripts run
    • Enhanced event logs with red-tool entries capturing command, output, status, and timing
  • Tests

    • Added coverage for constrained attack execution and event logging validation

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Warning

Rate limit exceeded

@Ker102 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 9 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0330a55-be4f-417b-9859-4ff3a342bf3f

📥 Commits

Reviewing files that changed from the base of the PR and between 19f1953 and 3adfa2a.

📒 Files selected for processing (5)
  • README.md
  • docs/case-study.md
  • docs/demo-script.md
  • src/nullstate/cli.py
  • tests/test_cli.py
📝 Walkthrough

Walkthrough

This PR implements constrained attack script execution for the nullstate purple-team CLI. It introduces an AttackToolResult dataclass to capture execution metadata, validates that only allowlisted attack.py scripts can execute from the run directory, updates scenario scripts to accept CLI arguments and perform health probes, integrates execution into the run workflow with red-tool event logging, and adds corresponding documentation and tests.

Changes

Constrained Attack Script Execution with Event Logging

Layer / File(s) Summary
Data Contracts
src/nullstate/attack_runner.py
Frozen AttackToolResult dataclass captures command, target URL, stage, return code, stdout/stderr, start/end timestamps, and duration. Includes ok property and to_dict() serialization.
Attack Script Execution
src/nullstate/attack_runner.py
run_attack_script() executes generated Python scripts via subprocess.run() with timeout and captured output. _validate_attack_script() enforces that only attack.py located directly in run directory is executable.
Attack Scenario Scripts
src/nullstate/attack.py
Azure and AWS scenarios now perform health probes to /_localstack/health when target URL is online (http/https), returning 0 for non-5xx and 2 for 5xx or network errors. Other scenarios parse --target-url and --stage arguments and exit with 0.
CLI Run Workflow
src/nullstate/cli.py
Integrates attack runner into run command for both before and after stages. Executes allowlisted script, emits red-tool events with execution evidence, and merges tool metadata into simulated attack results via helper functions.
Documentation
README.md, docs/case-study.md, docs/demo-script.md, docs/security-model.md
Documents expanded purple-team loop with red reasoning, constrained attack execution, blue remediation, and validation. Specifies red-tool event content and V1 runner constraints: single attack.py file, run directory location, Python interpreter execution, restricted CLI inputs.
Tests
tests/test_attack_runner.py, tests/test_cli.py
New AttackRunnerTests validates successful execution with metadata capture, rejects scripts outside run directory, and rejects non-attack-script filenames. Enhanced CLI test verifies red-tool events in events.jsonl for before and after stages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Constrained scripts now hop with care,
Attack probes dance in the run directory air,
Events logged as red-tools trace their way,
Before and after, defended each day! 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add constrained red attack runner' directly describes the main change: introducing a new constrained attack runner component.
Description check ✅ Passed The PR description covers the key objectives and includes a comprehensive test plan, but the template structure with explicit checkboxes is not followed exactly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/red-agent-runner

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 50: Confirm whether the "stage" field is actually emitted to events.jsonl
by the red tool runner (the generator that writes events.jsonl alongside
generated attack.py runs) and if so, update the README text to include "stage"
in the list of recorded fields; if it is not emitted, update the PR description
to remove "stage" from the objectives and/or add a note in the README explaining
why it is omitted. While editing the README sentence, consider splitting the
long sentence into 2–3 shorter sentences and ensure the links to Security Model
and Threat Model remain intact.

In `@src/nullstate/attack_runner.py`:
- Around line 53-60: The subprocess.run call in attack_runner.py (the block that
sets completed = subprocess.run(...)) can raise subprocess.TimeoutExpired; catch
subprocess.TimeoutExpired around that call and convert it into an
AttackToolResult instead of letting it propagate: import or reference
subprocess.TimeoutExpired, set a timed_out/timeout flag in the AttackToolResult,
populate stdout and stderr using the exception's output and stderr attributes
(or empty strings if missing), include the timeout_seconds and
command/resolved_run_dir in the result metadata, and return that
AttackToolResult; ensure existing success/failure code paths (which use the
completed variable) remain unchanged for non-timeout cases.

In `@src/nullstate/attack.py`:
- Around line 15-27: Duplicate probe_target logic found in multiple script
templates (probe_target in src/nullstate/attack.py and the same function in the
azure-public-blob and aws-public-s3 templates); extract it into a shared
template helper (e.g., a template fragment named probe_target_helper) and update
each script template to include or import that helper instead of embedding the
function inline. Ensure the shared helper preserves the same signature
(probe_target(target_url: str, stage: str) -> int), same behavior around URL
validation, health_url construction, urllib.request.urlopen usage and error
handling (return codes 0/2), and update the templates to reference the helper
symbol so changes are centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 415be843-8957-4d70-b2c3-415454300e1f

📥 Commits

Reviewing files that changed from the base of the PR and between 1edcd05 and 19f1953.

📒 Files selected for processing (9)
  • README.md
  • docs/case-study.md
  • docs/demo-script.md
  • docs/security-model.md
  • src/nullstate/attack.py
  • src/nullstate/attack_runner.py
  • src/nullstate/cli.py
  • tests/test_attack_runner.py
  • tests/test_cli.py

Comment thread README.md
## Security model

V1 does not target real cloud environments by default. Sandboxes are explicit, run artifacts are local, and remediation happens in a copied run workspace rather than mutating the original Terraform directory. See [Security Model](docs/security-model.md) and [Threat Model](docs/threat-model.md).
V1 does not target real cloud environments by default. Sandboxes are explicit, run artifacts are local, and remediation happens in a copied run workspace rather than mutating the original Terraform directory. The red tool runner is constrained to generated `attack.py` scripts inside the run directory and records command, stdout, stderr, return code, target URL, and timestamps in `events.jsonl`. See [Security Model](docs/security-model.md) and [Threat Model](docs/threat-model.md).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify the omission of "stage" field in events.jsonl documentation.

The PR objectives state that events.jsonl records "command, stdout, stderr, return code, target URL, stage, and timestamps," but this line omits "stage" from the documented fields. Please verify whether "stage" should be included in the documentation.


Optional: Consider breaking the long sentence for readability.

The security model sentence spans 71 words with multiple clauses. While grammatically correct, splitting it into 2-3 shorter sentences could improve scannability.

✍️ Suggested readability improvement
-V1 does not target real cloud environments by default. Sandboxes are explicit, run artifacts are local, and remediation happens in a copied run workspace rather than mutating the original Terraform directory. The red tool runner is constrained to generated `attack.py` scripts inside the run directory and records command, stdout, stderr, return code, target URL, and timestamps in `events.jsonl`. See [Security Model](docs/security-model.md) and [Threat Model](docs/threat-model.md).
+V1 does not target real cloud environments by default. Sandboxes are explicit, run artifacts are local, and remediation happens in a copied run workspace rather than mutating the original Terraform directory. 
+
+The red tool runner is constrained to generated `attack.py` scripts inside the run directory. It records command, stdout, stderr, return code, target URL, and timestamps in `events.jsonl`. 
+
+See [Security Model](docs/security-model.md) and [Threat Model](docs/threat-model.md).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 50, Confirm whether the "stage" field is actually emitted
to events.jsonl by the red tool runner (the generator that writes events.jsonl
alongside generated attack.py runs) and if so, update the README text to include
"stage" in the list of recorded fields; if it is not emitted, update the PR
description to remove "stage" from the objectives and/or add a note in the
README explaining why it is omitted. While editing the README sentence, consider
splitting the long sentence into 2–3 shorter sentences and ensure the links to
Security Model and Threat Model remain intact.

Comment on lines +53 to +60
completed = subprocess.run(
command,
cwd=resolved_run_dir,
text=True,
capture_output=True,
check=False,
timeout=timeout_seconds,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Handle subprocess.TimeoutExpired exception.

The subprocess.run call can raise subprocess.TimeoutExpired when the timeout is exceeded, but this exception is not caught. Callers expect an AttackToolResult regardless of timeout, so the exception should be caught and converted to a result with appropriate metadata.

🛡️ Proposed fix to handle timeout
+import subprocess
+
 def run_attack_script(
     script_path: Path,
     *,
     run_dir: Path,
     target_url: str,
     stage: str,
     timeout_seconds: int = 30,
 ) -> AttackToolResult:
     resolved_script = script_path.resolve()
     resolved_run_dir = run_dir.resolve()
     _validate_attack_script(resolved_script, resolved_run_dir)
 
     command = [
         sys.executable,
         str(resolved_script),
         "--target-url",
         target_url,
         "--stage",
         stage,
     ]
     started_at = datetime.now(UTC).isoformat()
     started = time.monotonic()
-    completed = subprocess.run(
-        command,
-        cwd=resolved_run_dir,
-        text=True,
-        capture_output=True,
-        check=False,
-        timeout=timeout_seconds,
-    )
+    try:
+        completed = subprocess.run(
+            command,
+            cwd=resolved_run_dir,
+            text=True,
+            capture_output=True,
+            check=False,
+            timeout=timeout_seconds,
+        )
+    except subprocess.TimeoutExpired as exc:
+        ended_at = datetime.now(UTC).isoformat()
+        return AttackToolResult(
+            command=command,
+            target_url=target_url,
+            stage=stage,
+            returncode=-1,
+            stdout=exc.stdout or "",
+            stderr=exc.stderr or "",
+            started_at=started_at,
+            ended_at=ended_at,
+            duration_seconds=round(time.monotonic() - started, 3),
+        )
     ended_at = datetime.now(UTC).isoformat()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nullstate/attack_runner.py` around lines 53 - 60, The subprocess.run call
in attack_runner.py (the block that sets completed = subprocess.run(...)) can
raise subprocess.TimeoutExpired; catch subprocess.TimeoutExpired around that
call and convert it into an AttackToolResult instead of letting it propagate:
import or reference subprocess.TimeoutExpired, set a timed_out/timeout flag in
the AttackToolResult, populate stdout and stderr using the exception's output
and stderr attributes (or empty strings if missing), include the timeout_seconds
and command/resolved_run_dir in the result metadata, and return that
AttackToolResult; ensure existing success/failure code paths (which use the
completed variable) remain unchanged for non-timeout cases.

Comment thread src/nullstate/attack.py
Comment on lines +15 to +27
def probe_target(target_url: str, stage: str) -> int:
print(f"stage={stage} target={target_url}")
if not target_url.startswith(("http://", "https://")):
print("offline target selected; no network request performed")
return 0
health_url = target_url.rstrip("/") + "/_localstack/health"
try:
with urllib.request.urlopen(health_url, timeout=5) as response:
print(f"health_url={health_url} status={response.status}")
return 0 if response.status < 500 else 2
except urllib.error.URLError as error:
print(f"health_url={health_url} error={error}")
return 2
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider extracting duplicate probe_target logic.

The probe_target function appears identically in both the azure-public-blob and aws-public-s3 script templates. While the duplication is currently manageable since these are embedded script templates written to standalone files, consider extracting shared logic into a template helper if the script library grows or diverges.

Also applies to: 47-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/nullstate/attack.py` around lines 15 - 27, Duplicate probe_target logic
found in multiple script templates (probe_target in src/nullstate/attack.py and
the same function in the azure-public-blob and aws-public-s3 templates); extract
it into a shared template helper (e.g., a template fragment named
probe_target_helper) and update each script template to include or import that
helper instead of embedding the function inline. Ensure the shared helper
preserves the same signature (probe_target(target_url: str, stage: str) -> int),
same behavior around URL validation, health_url construction,
urllib.request.urlopen usage and error handling (return codes 0/2), and update
the templates to reference the helper symbol so changes are centralized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant