Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| Doing | Command / skill |
|---|---|
| Full audit | `/axguard-audit` or skill `axguard-audit` |
| Security Diff | `/axguard-diff` · `axguard diff` · MCP `axguard_security_diff` |
| Security lead pass | skill `axguard-cso` |
| Triage | `/axguard-triage` |
| Fix | `/axguard-fix` / skill `axguard-remediate` |
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pip install -e .
| What you are doing | Start here |
|---|---|
| About to publish | `/axguard-audit` |
| Security Diff on a change | `/axguard-diff` · `axguard diff` |
| Quick check while coding | `/axguard-scan` |
| New / unknown codebase | `/axguard-threat-model` → `/axguard-audit` |
| Secrets | `/axguard-secrets` |
Expand Down
3 changes: 3 additions & 0 deletions COMMANDS-QUICK-REF.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

| Doing this | Run |
|---|---|
| **Pre-Ship gate (ship / no-ship)** | `axguard preship .` → [docs/preship.md](docs/preship.md) |
| **Security Diff (what changed?)** | `axguard diff` / `axguard security-diff` → [docs/security-diff.md](docs/security-diff.md) |
| Full pre-ship audit + HTML/MD | `/axguard-audit` |
| Fast scan | `/axguard-scan` |
| Map app / attack surface | `/axguard-surface` |
Expand All @@ -16,6 +18,7 @@
| Investigation Agent | `axguard investigate …` |
| GitHub Security Bot | `axguard github setup\|validate\|test\|status` |
| Predictive Security | `axguard predict …` |
| Security Diff (what changed?) | `axguard diff` · `/axguard-diff` → [docs/security-diff.md](docs/security-diff.md) |
| Local Security Intelligence API | `axguard api start` → `http://127.0.0.1:8787` |
| MCP (AI coding agents) | `axguard mcp` · `serve` · `doctor` · `tools` → [docs/mcp.md](docs/mcp.md) |
| Training-data pipeline | `/axguard-data` |
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ Pre-ship security gate — not a full pentest platform. Scan source, triage nois

---

## Pre-Ship Security

Find → Explain → Fix → Verify → Ship.

```bash
axguard preship .
```

AXGuard analyzes security-sensitive changes, verifies findings, checks attack paths and security regressions, and tells you whether the application is ready to ship.

- Pre-Ship: [docs/preship.md](docs/preship.md)
- Security Diff: [docs/security-diff.md](docs/security-diff.md)

---

## What is AXguard?

**AXguard is a pre-ship security gate.**
Expand Down Expand Up @@ -222,6 +237,8 @@ open .findings/axguard/axguard-report.html
| Security Memory | `axguard memory record .` → [docs/memory](docs/memory/README.md) |
| Investigation Agent | `axguard investigate .` → [docs/investigation](docs/investigation/README.md) |
| Predictive security risk | `axguard predict .` → [docs/predictive](docs/predictive/README.md) |
| Security Diff | `axguard diff` → [docs/security-diff.md](docs/security-diff.md) |
| Pre-Ship gate | `axguard preship .` → [docs/preship.md](docs/preship.md) |
| Local Security Intelligence API | `axguard api start` → [docs/api](docs/api/overview.md) |
| MCP for AI coding agents | `axguard mcp` → [docs/mcp.md](docs/mcp.md) |
| GitHub PR bot (self-host) | `axguard github setup` → [docs/github](docs/github/README.md) |
Expand Down Expand Up @@ -357,6 +374,16 @@ axguard predict --agent
axguard predict --mcp
axguard predict --what-if

# Security Diff (security-aware comparison of two versions)
axguard diff
axguard diff HEAD~1
axguard diff main...HEAD
axguard diff --base main --head HEAD
axguard diff --json
axguard diff --verbose
axguard diff --fail-on high
axguard diff baseline save

# Training-data pipeline (no model training)
axguard data discover
axguard data inspect
Expand Down
215 changes: 213 additions & 2 deletions cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,82 @@ def _mem_common(p: argparse.ArgumentParser) -> None:
help="Skip engagement / first-run messaging",
)

diff_cmd = sub.add_parser(
"diff",
aliases=["security-diff"],
help=(
"Security Diff — compare two application states and explain "
"security-relevant changes (not another scanner)"
),
)
# Use nargs='*' tokens (not nested subparsers) so `axguard diff HEAD~1` works.
# Tokens: [range|ref|path] OR `baseline` [save] [path]
diff_cmd.add_argument(
"tokens",
nargs="*",
default=[],
help=(
"Git range (main...HEAD), ref (HEAD~1), path, or "
"'baseline' / 'baseline save' [--name]"
),
)
diff_cmd.add_argument("--base", default=None, help="Base git ref or path")
diff_cmd.add_argument("--head", default=None, help="Head git ref or path (default: working tree)")
diff_cmd.add_argument(
"--fail-on",
choices=("critical", "high", "medium", "low", "none"),
default="none",
help="Exit non-zero when security impact meets this level",
)
diff_cmd.add_argument(
"--json",
action="store_true",
dest="as_json",
help="Print Security Diff JSON",
)
diff_cmd.add_argument(
"--verbose",
action="store_true",
help="Detailed human-readable delta",
)
diff_cmd.add_argument(
"--html",
action="store_true",
help="Write HTML report under --out-dir",
)
diff_cmd.add_argument(
"--out-dir",
default=".findings/axguard",
help="Artifact directory (default: .findings/axguard)",
)
diff_cmd.add_argument(
"--no-incremental",
action="store_true",
help="Disable incremental/changed-file scoping metadata",
)
diff_cmd.add_argument(
"--investigate",
action="store_true",
help="Soft-invoke Investigation Engine for control-removal candidates",
)
diff_cmd.add_argument(
"--baseline-name",
default="default",
dest="baseline_name",
help="AXGuard snapshot baseline name (non-git)",
)
diff_cmd.add_argument(
"--name",
default=None,
help="Alias for --baseline-name (baseline save/compare)",
)
diff_cmd.add_argument("--no-banner", action="store_true", help="Hide the ASCII banner")
diff_cmd.add_argument(
"--no-engage",
action="store_true",
help="Skip engagement / first-run messaging",
)

sub.add_parser("version", help="Print version")
sub.add_parser("help", help="Show Start Using workflow table")

Expand Down Expand Up @@ -770,6 +846,13 @@ def _gh_common(p: argparse.ArgumentParser) -> None:
except ImportError:
pass

try:
from engines.preship.cli import add_preship_parser

add_preship_parser(sub)
except ImportError:
pass

return parser


Expand All @@ -778,7 +861,9 @@ def _gh_common(p: argparse.ArgumentParser) -> None:

What you are doing Command
----------------------------- -------------------------
About to publish / open a PR axguard audit . | /axguard-audit
About to publish / open a PR axguard preship . | axguard audit .
Pre-ship gate (ship / no-ship) axguard preship . | /axguard-preship
Security Diff (what changed?) axguard diff [BASE] | axguard security-diff
Quick check while coding axguard scan . | /axguard-scan
Map attack surface / app model axguard surface . | /axguard-surface
Dataflow / taint paths axguard flow . | /axguard-flow
Expand Down Expand Up @@ -806,13 +891,16 @@ def _gh_common(p: argparse.ArgumentParser) -> None:
Triage → fix → report → CI /axguard-triage · /axguard-fix · /axguard-report · /axguard-ci

Pipeline:
Find → Explain → Fix → Verify → Ship
threat-model → audit → triage → fix → report → ci
preship (gate) · diff (change impact)

Reports land in:
.findings/axguard/axguard-report.{html,md,json}
.findings/axguard/preship/preship-report.{html,md,json}

Cheat sheet: COMMANDS-QUICK-REF.md
Docs: docs/engagement.md · docs/contributors/README.md (local prefs, no telemetry)
Docs: docs/preship.md · docs/security-diff.md · docs/engagement.md
""".strip()


Expand Down Expand Up @@ -1333,6 +1421,14 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "predict":
return _run_predict_command(args)

if args.command in {"diff", "security-diff"}:
return _run_diff_command(args)

if args.command in {"preship"}:
from engines.preship.cli import run_preship_command

return int(run_preship_command(args))

parser.print_help()
return 2

Expand All @@ -1356,6 +1452,121 @@ def _run_github_command(args: argparse.Namespace) -> int:
return 2


def _run_diff_command(args: argparse.Namespace) -> int:
"""Security Diff CLI — orchestrates existing engines, does not scan alone."""
if not getattr(args, "no_banner", False):
print_banner(compact=True)
print()

from engines.security_diff import (
run_security_diff,
save_baseline_from_project,
should_fail,
)
from engines.security_diff.report import (
render_text,
to_json,
)

tokens = list(getattr(args, "tokens", None) or [])
name = (
getattr(args, "name", None)
or getattr(args, "baseline_name", None)
or "default"
)

# axguard diff baseline [save] [path]
if tokens and tokens[0] == "baseline":
rest = tokens[1:]
if rest and rest[0] == "save":
path = Path((rest[1] if len(rest) > 1 else ".") or ".").resolve()
out = save_baseline_from_project(path, name=name)
print(f"Saved Security Diff baseline '{name}' → {out}")
return 0
# axguard diff baseline → compare to snapshot
project = rest[0] if rest else "."
result = run_security_diff(
project=project,
use_snapshot=True,
baseline_name=name,
fail_on=getattr(args, "fail_on", "none") or "none",
write_report=False,
)
if getattr(args, "as_json", False):
print(to_json(result), end="")
else:
print(render_text(result), end="")
return 1 if should_fail(result, getattr(args, "fail_on", "none") or "none") else 0

range_or_path = tokens[0] if tokens else None
base = getattr(args, "base", None)
head = getattr(args, "head", None)
range_spec = None
project = "."

if range_or_path:
if range_or_path in {".", "./"} or Path(range_or_path).exists():
project = range_or_path
# Non-git path compare against snapshot when no --base
if base is None:
result = run_security_diff(
project=project,
use_snapshot=True,
baseline_name=name,
fail_on=args.fail_on,
incremental=not args.no_incremental,
investigate=bool(args.investigate),
out_dir=args.out_dir,
write_report=bool(args.html),
)
return _emit_diff_result(args, result)
elif "..." in range_or_path or ".." in range_or_path:
range_spec = range_or_path
else:
base = base or range_or_path

result = run_security_diff(
project=project,
base=base,
head=head,
range_spec=range_spec,
baseline_name=name,
fail_on=args.fail_on,
incremental=not args.no_incremental,
investigate=bool(args.investigate),
out_dir=args.out_dir,
write_report=bool(args.html),
)
return _emit_diff_result(args, result)


def _emit_diff_result(args: argparse.Namespace, result: dict) -> int:
from engines.security_diff import should_fail
from engines.security_diff.report import (
render_text,
render_verbose,
to_json,
write_security_diff_report,
)

if getattr(args, "html", False) and not getattr(args, "as_json", False):
paths = write_security_diff_report(result, Path(args.out_dir))
print(f"Wrote Security Diff report → {paths.get('html')}")
if getattr(args, "as_json", False):
print(to_json(result), end="")
elif getattr(args, "verbose", False):
print(render_verbose(result), end="")
else:
print(render_text(result), end="")

if not getattr(args, "no_engage", False):
try:
_print_engagement(emit_for_paths(result))
except Exception: # noqa: BLE001
pass
return 1 if should_fail(result, getattr(args, "fail_on", "none") or "none") else 0


def _run_predict_command(args: argparse.Namespace) -> int:
"""Predictive Security Intelligence CLI."""
import json as _json
Expand Down
22 changes: 22 additions & 0 deletions commands/axguard-diff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: axguard-diff
description: Run AXGuard Security Diff — compare two versions and explain security-relevant changes (attack surface, controls, attack paths, privileges, regressions). Use after meaningful security-sensitive changes, not trivial edits.
---

# /axguard-diff

Compare BASE vs HEAD from a **security** perspective.

```bash
axguard diff
axguard diff HEAD~1
axguard diff main...HEAD
axguard diff --base main --head HEAD
axguard diff --json
axguard diff --verbose
axguard diff --fail-on high
```

MCP: `axguard_security_diff`

See [docs/security-diff.md](../docs/security-diff.md).
5 changes: 5 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Annotations are MCP **hints** (`readOnlyHint`, `destructiveHint`, `idempotentHin

## Primary

### `axguard_preship` / `axguard_security_diff`

Pre-Ship gate and Security Diff. Prefer these when asking "can I ship?" or "what became more dangerous?". Details: [preship.md](preship.md) · [security-diff.md](security-diff.md).

### `axguard_security_review`

**Approval:** APPROVAL_REQUIRED (esp. `DEEP` / `MAX`) · **Annotations:** `readOnlyHint=true`, `destructiveHint=false`, `idempotentHint=true`, `openWorldHint=false`
Expand Down Expand Up @@ -136,6 +140,7 @@ Never invent evidence from model speculation.
|---|---|---|---|
| `axguard_predict_security_risks` | AUTO | RO, idempotent | Risk **expansion** signals — never label as confirmed vulns. |
| `axguard_analyze_change_risk` | AUTO | RO, idempotent | Change-focused predictive view (PR/diff). |
| `axguard_security_diff` | AUTO | RO, idempotent | Compare two app states — attack surface, controls, data flows, privileges, trust boundaries, attack paths, regressions, predictive risks. |

---

Expand Down
Loading