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
231 changes: 229 additions & 2 deletions cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,104 @@ 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)"
),
)
diff_sub = diff_cmd.add_subparsers(dest="diff_command")
diff_cmd.add_argument(
"range_or_path",
nargs="?",
default=None,
help="Git range (main...HEAD), ref (HEAD~1), commit, or path (default: auto)",
)
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",
help="AXGuard snapshot baseline name (non-git)",
)
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",
)
diff_base = diff_sub.add_parser(
"baseline",
help="Compare against a stored AXGuard baseline (or save one)",
)
diff_base_sub = diff_base.add_subparsers(dest="diff_baseline_command")
diff_base.add_argument(
"--name",
default="default",
help="Baseline name (default: default)",
)
diff_base.add_argument(
"--json",
action="store_true",
dest="as_json",
help="Print JSON",
)
diff_base.add_argument("--no-banner", action="store_true", help="Hide the ASCII banner")
diff_base_save = diff_base_sub.add_parser(
"save",
help="Save current security state as a baseline snapshot",
)
diff_base_save.add_argument(
"--name",
default="default",
help="Baseline name (default: default)",
)
diff_base_save.add_argument(
"path",
nargs="?",
default=".",
help="Project path (default: .)",
)

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

Expand Down Expand Up @@ -770,6 +868,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 +883,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 +913,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 +1443,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 +1474,115 @@ 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,
render_verbose,
to_json,
write_security_diff_report,
)

# axguard diff baseline save
if getattr(args, "diff_command", None) == "baseline":
name = getattr(args, "name", None) or "default"
if getattr(args, "diff_baseline_command", None) == "save":
path = Path(getattr(args, "path", ".") 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
result = run_security_diff(
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 = getattr(args, "range_or_path", 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=getattr(args, "baseline_name", "default") or "default",
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=getattr(args, "baseline_name", "default") or "default",
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).
Loading