Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CI and repository security workflows for this project.
| Workflow | Purpose |
|---|---|
| `ci.yml` | Unit tests, skill validation, fixture self-scan |
| `axguard.yml` | AXGuard Security Review on PRs / main (`audit --fail-on high`) |
| `axguard.yml` | AXGuard Security Review on PRs / main (`audit engines` + `cli` `--fail-on high`; fixtures excluded) |
| `codeql.yml` | CodeQL static analysis (Python) |
| `gitleaks.yml` | Secret detection (Gitleaks CLI) |
| `osv-scanner.yml` | Dependency vulns via OSV |
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/axguard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@ jobs:
- name: Install AXGuard
run: python -m pip install -e .

# Dogfood shipped runtime code. fixtures/, skills/, commands/, and rules/
# intentionally contain vulnerable examples for regression tests.
- name: Security review (fail on high+)
run: axguard audit . --fail-on high --no-banner
run: |
axguard audit engines --fail-on high --no-banner
axguard audit cli --fail-on high --no-banner
4 changes: 4 additions & 0 deletions COMMANDS-QUICK-REF.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
| Regenerate reports | `/axguard-report` |
| Add CI gate | `/axguard-ci` |
| About / engagement prefs | `axguard about` · `axguard engage disable` |
| Contribute / privacy (local) | `/axguard-contribute` · `/axguard-privacy` |

## Default pipeline

Expand Down Expand Up @@ -91,6 +92,9 @@ axguard investigate . --finding FINDING_ID
axguard investigate --explain FINDING_ID
axguard about
axguard engage disable
axguard privacy status
axguard contribute status
# slash: /axguard-privacy · /axguard-contribute
axguard data discover
axguard data report fixtures/data_pipeline
axguard twin build . # Security Twin — see docs/twin/README.md
Expand Down
46 changes: 44 additions & 2 deletions cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,11 @@ def _mem_common(p: argparse.ArgumentParser) -> None:
engage_sub.add_parser("enable", help="Re-enable promotional messaging")
engage_sub.add_parser("dismiss", help="Dismiss the latest support ask (cooldown)")

from engines.contributors.cli import add_contribute_parser, add_privacy_parser

add_privacy_parser(sub)
add_contribute_parser(sub)

github_cmd = sub.add_parser(
"github",
help="GitHub Security Bot — setup / validate / test / status",
Expand Down Expand Up @@ -782,6 +787,7 @@ def _gh_common(p: argparse.ArgumentParser) -> None:
GitHub Security Bot axguard github … | docs/github/README.md
About AXGuard axguard about
Engagement prefs axguard engage disable | enable | dismiss
Contribute / privacy (local) axguard contribute … · axguard privacy …
First look at a new codebase /axguard-threat-model → /axguard-audit
Secrets / auth / inject /axguard-secrets · /axguard-auth · /axguard-inject
SQL / SSTI / path /axguard-sql · /axguard-ssti · /axguard-path
Expand All @@ -797,7 +803,7 @@ def _gh_common(p: argparse.ArgumentParser) -> None:
.findings/axguard/axguard-report.{html,md,json}

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


Expand Down Expand Up @@ -885,6 +891,16 @@ def main(argv: list[str] | None = None) -> int:
return 2
return 0

if args.command == "privacy":
from engines.contributors.cli import run_privacy_command

return run_privacy_command(args)

if args.command == "contribute":
from engines.contributors.cli import run_contribute_command

return run_contribute_command(args)

if args.command == "github":
if not getattr(args, "no_banner", False):
print_banner()
Expand Down Expand Up @@ -1035,7 +1051,18 @@ def main(argv: list[str] | None = None) -> int:
print()
print(render_report(result, "md"))
if not getattr(args, "no_engage", False):
_print_engagement(emit_after_audit(result))
eng = emit_after_audit(result)
if eng:
_print_engagement(eng)
else:
from engines.contributors.hooks import emit_after_audit_soft

_print_engagement(
emit_after_audit_soft(
result,
no_engage=False,
)
)
return 1 if _should_fail(result["findings"], args.fail_on) else 0

if args.command == "surface":
Expand Down Expand Up @@ -1134,6 +1161,21 @@ def main(argv: list[str] | None = None) -> int:
print(f" REQUIRES_REVIEW {summary.get('REQUIRES_REVIEW', 0)}")
print(f" json {paths['json']}")
print(f" md {paths['markdown']}")
if not getattr(args, "no_engage", False):
from engines.contributors.hooks import emit_after_adversary_soft

# Shape result like audit context helpers expect
soft_result = {
**adv_result,
"adversary_summary": summary,
"findings": adv_result.get("findings") or [],
}
_print_engagement(
emit_after_adversary_soft(
soft_result,
no_engage=False,
)
)
return 0

if args.command == "evidence":
Expand Down
43 changes: 43 additions & 0 deletions commands/axguard-contribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
description: Contextual contribution invites and local prepare (no auto-push). Usage: /axguard-contribute …
---

# /axguard-contribute

**Specialist:** Contributor Engagement & Learning

Recognizes meaningful local work, suggests a contribution type when quality is
strong, and prepares a **local** package (templates, commit/PR draft text).
Never pushes or opens a PR. No dark patterns — cooldown and dismiss controls.

## Usage

```
/axguard-contribute status
/axguard-contribute suggest [--context-json FILE] [--force]
/axguard-contribute prepare [--type TYPE] [--context-json FILE] [--learning]
/axguard-contribute dismiss not_now|never_prompts|type [--type TYPE]
/axguard-contribute milestones
/axguard-contribute templates
```

CLI equivalent: `axguard contribute <subcommand>`.

## Focus

- Opportunity score from observable work (not personal worth)
- Quality gate: novelty, reusability, relevance, testability
- Cooldown: at most one invite per meaningful session
- Dismiss: `not_now` / `never_prompts` / type
- Prepare writes under `.findings/axguard/contribute/<id>/`
- Scrubbing via `engines.data.scrub`; learning copy needs `--learning` + opt-in

## Steps

1. `axguard privacy opt-in` (required before prepare)
2. `axguard contribute suggest` after a useful audit/adversary session
3. Review the invite; dismiss if not wanted
4. `axguard contribute prepare` — inspect the package locally
5. Share / push / PR only under your own control (GitHub OAuth is future work)

See [docs/contributors/README.md](../docs/contributors/README.md).
42 changes: 42 additions & 0 deletions commands/axguard-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
description: Local privacy prefs for contribution learning (opt-in). Usage: /axguard-privacy …
---

# /axguard-privacy

**Specialist:** Contributor privacy center

Shows what AXGuard stores for contribution learning, what stays local, and how
to opt in/out, export, or delete. Defaults are off — no network, no telemetry.

## Usage

```
/axguard-privacy status
/axguard-privacy show
/axguard-privacy opt-in
/axguard-privacy opt-in --learning
/axguard-privacy opt-out
/axguard-privacy export -o .findings/axguard/privacy-export.json
/axguard-privacy delete
/axguard-privacy reset
```

CLI equivalent: `axguard privacy <subcommand>`.

## Focus

- Prefs live at `~/.axguard/privacy.json`
- Packaging and learning require explicit opt-in
- Included vs excluded field lists (no credentials, tokens, private repo IDs)
- Export / delete / reset for local learning data only
- GitHub push/PR is never automatic

## Steps

1. `axguard privacy status` — current opt-in and learning flags
2. `axguard privacy show` — included/excluded fields and notes
3. Opt in only when you intend to prepare a local contribution package
4. `axguard privacy delete` or `reset` to clear local learning copies

See [docs/contributors/README.md](../docs/contributors/README.md).
76 changes: 76 additions & 0 deletions docs/contributors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Contributor Engagement & Learning

Transparent, respectful, **opt-in**, privacy-preserving contribution prompts and local packaging.

AXGuard may recognize useful local work (verified findings, false-positive rejections, regressions, attack paths) and optionally invite a contribution. Scoring measures whether a **contribution opportunity** exists — never personal worth, streaks, or rankings.

## Principles

1. **Opt-in** — packaging and learning stay off until you say so.
2. **Local by default** — no network or telemetry for learning.
3. **Prepare only** — never silent push or PR. GitHub OAuth / remote submit is future work.
4. **No dark patterns** — no fake praise, guilt, fake urgency, or fake leaderboards.
5. **Reuse** — scrubbing uses `engines.data.scrub`; messaging reuses the engagement engine.

## Journey

```text
observable work → opportunity signal → quality gate → recognition / invite
↓ (explicit)
axguard contribute prepare
↓
.findings/axguard/contribute/<id>/
```

Dismissal options:

- `not_now` — cooldown; no more invites this session
- `never_prompts` — stop contribution prompts
- `type` — never suggest that contribution type again

## Privacy

Prefs: `~/.axguard/privacy.json`

```bash
axguard privacy status
axguard privacy show # includes included/excluded field lists
axguard privacy opt-in
axguard privacy opt-in --learning # local learning copy only; still no upload
axguard privacy opt-out
axguard privacy export -o .findings/axguard/privacy-export.json
axguard privacy delete # remove local learning data
axguard privacy reset # prefs → defaults + clear learning data
```

Defaults:

```yaml
contributions:
enabled: true
prompts: true
auto_prepare: false
auto_push: false
auto_pr: false
learning: false # opt-in
learning:
contribution_data:
enabled: false
```

## CLI

```bash
axguard contribute status
axguard contribute suggest [--context-json FILE] [--force]
axguard contribute prepare [--type TYPE] [--context-json FILE] [--learning]
axguard contribute dismiss not_now|never_prompts|type [--type TYPE]
axguard contribute milestones
axguard contribute templates
```

## Future (not in this MVP)

- GitHub OAuth App / authenticated PR creation
- Remote learning submit
- Hosted contribution dashboards
4 changes: 4 additions & 0 deletions docs/engagement.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ axguard paths . --no-engage
2. **No dark patterns** — no urgency, fake social proof, or invented YC status.
3. **Local only** — state file is optional; delete it to reset.
4. **One message** — CLI prints at most one engagement block after the real report.

## Contributors

Optional contribution recognition and local packaging live in a separate engine. See [docs/contributors/README.md](contributors/README.md) for privacy defaults, `axguard privacy` / `axguard contribute` CLI, and the prepare-local-only rule (no auto push/PR).
14 changes: 6 additions & 8 deletions engines/api/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,10 @@ def update_project(self, project_id: str, **fields: Any) -> dict[str, Any] | Non
if not updates:
return self.get_project(project_id)
updates["updated_at"] = time.time()
# Column names are allowlisted above; values stay bound parameters.
sets = ", ".join(f"{k} = ?" for k in updates)
self._execute(
f"UPDATE projects SET {sets} WHERE id = ?",
tuple(updates.values()) + (project_id,),
)
sql = "UPDATE projects SET " + sets + " WHERE id = ?"
self._execute(sql, tuple(updates.values()) + (project_id,))
return self.get_project(project_id)

def delete_project(self, project_id: str) -> bool:
Expand Down Expand Up @@ -418,11 +417,10 @@ def update_scan(self, scan_id: str, **fields: Any) -> dict[str, Any] | None:
updates[k] = v
if not updates:
return self.get_scan(scan_id)
# Column names are allowlisted above; values stay bound parameters.
sets = ", ".join(f"{k} = ?" for k in updates)
self._execute(
f"UPDATE scans SET {sets} WHERE id = ?",
tuple(updates.values()) + (scan_id,),
)
sql = "UPDATE scans SET " + sets + " WHERE id = ?"
self._execute(sql, tuple(updates.values()) + (scan_id,))
return self.get_scan(scan_id)

def cancel_scan(self, scan_id: str) -> dict[str, Any] | None:
Expand Down
Loading