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
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ agsec install claude-code # activate the firewall
Done. Every tool call is now checked against your policies. Out of the box, the following are blocked:

- `rm`, `rm -rf`, `rmdir` — destructive deletes
- Writes to `.env`, `.env.*`, secrets files
- `git push --force` — force pushes
- `DROP TABLE`, `DELETE FROM` without WHERE — destructive SQL
- Reads of `~/.ssh`, `~/.aws`, `~/.gnupg` — credential directories
- Reads and writes to `.env`, credentials, SSH keys, cloud credentials
- `git push --force`, `git reset --hard` — destructive git
- `DROP TABLE`, `TRUNCATE`, `ALTER DROP` — DDL commands
- `DELETE FROM`, `UPDATE SET`, `INSERT INTO` — DML commands
- `sqlite3 audit.db`, `psql audit.db` — audit database tampering
- `chmod 777`, `mkfs`, `dd`, `shred` — destructive filesystem ops
- Direct push to `main`, `master`, `production` branches

---

Expand All @@ -73,7 +76,7 @@ agsec audit --stats # see what would have been blocked
agsec enforce # start blocking when ready
```

Observe mode gives you a full audit trail of every action your agent attempted — with zero disruption to your workflow. See the blast radius before you enforce it.
Observe mode gives you a full audit trail of every action your agent attempted — with zero disruption to your workflow. See the blast radius before you enforce it. Every action is logged with its actual outcome, so `agsec analyze` accurately shows what got through vs what would have been blocked.

---

Expand Down Expand Up @@ -110,7 +113,7 @@ statements:
actions: ["bash.execute"]
```

Three effects: `allow`, `deny`, `review` (human-in-the-loop pause). Deny always wins — same evaluation logic as AWS IAM. Layered policy evaluation (project + agent layers) where each layer is a gate. Supports 14 condition operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`, `regex`, `exists`, `not_exists`.
Three effects: `allow`, `deny`, `review` (human-in-the-loop pause). Deny always wins — same evaluation logic as AWS IAM. Layered policy evaluation (project + agent layers) where each layer is a gate. 21 built-in threat patterns for blast radius analysis. Supports 14 condition operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`, `regex`, `exists`, `not_exists`.

---

Expand Down Expand Up @@ -182,6 +185,8 @@ agsec validate # check for errors

agsec audit [--stats] # view action log
agsec analyze [--hours N] # threat analysis with blast radius
agsec analyze --all # full activity report (every action)
agsec status # firewall status at a glance
agsec observe # switch to observe mode
agsec enforce # switch to enforce mode

Expand Down
2 changes: 2 additions & 0 deletions agsec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__version__ = "0.2.2"

from .audit import AuditStore
from .control import ControlLayer
from .guard import guard
Expand Down
46 changes: 41 additions & 5 deletions agsec/audit/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def __init__(self, db_path: Optional[str] = None):

self.conn.row_factory = sqlite3.Row
self._init_db()
self._auto_prune()

def __enter__(self):
return self
Expand All @@ -60,15 +61,25 @@ def _init_db(self) -> None:
policy_status TEXT NOT NULL,
policy_reason TEXT,
context TEXT,
error TEXT
error TEXT,
outcome TEXT
)
""")
self.conn.execute("""
CREATE INDEX IF NOT EXISTS idx_executions_timestamp
ON executions(timestamp)
""")
# Migrate existing databases that don't have the outcome column
try:
self.conn.execute("SELECT outcome FROM executions LIMIT 1")
except sqlite3.OperationalError:
self.conn.execute("ALTER TABLE executions ADD COLUMN outcome TEXT")
self.conn.commit()

def log_execution(self, execution: ActionExecutionResult, context: Optional[Dict[str, Any]] = None, error: Optional[str] = None) -> None:
def log_execution(self, execution: ActionExecutionResult, context: Optional[Dict[str, Any]] = None, error: Optional[str] = None, outcome: Optional[str] = None) -> None:
self.conn.execute("""
INSERT INTO executions (timestamp, action, params, result, policy_status, policy_reason, context, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO executions (timestamp, action, params, result, policy_status, policy_reason, context, error, outcome)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
datetime.utcnow().isoformat(),
execution.action,
Expand All @@ -77,7 +88,8 @@ def log_execution(self, execution: ActionExecutionResult, context: Optional[Dict
execution.policy.status.value,
execution.policy.reason,
json.dumps(context, default=str) if context else None,
error
error,
outcome,
))
self.conn.commit()

Expand Down Expand Up @@ -127,6 +139,30 @@ def get_executions_since(
rows = self.conn.execute(query, params).fetchall()
return [dict(row) for row in rows]

def prune(self, days: int = 7) -> int:
"""Delete audit records older than N days. Returns count deleted."""
cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat()
cursor = self.conn.execute("DELETE FROM executions WHERE timestamp < ?", (cutoff,))
self.conn.commit()
return cursor.rowcount

def clear(self) -> int:
"""Delete ALL audit records. Returns count deleted."""
cursor = self.conn.execute("DELETE FROM executions")
self.conn.commit()
return cursor.rowcount

def _auto_prune(self) -> None:
"""Auto-prune if AGSEC_AUDIT_RETENTION_DAYS is set."""
retention = os.environ.get("AGSEC_AUDIT_RETENTION_DAYS")
if retention is not None:
try:
days = int(retention)
if days > 0:
self.prune(days=days)
except ValueError:
pass

def export_to_json(self, file_path: str) -> None:
executions = self.get_executions(limit=10000)
# Write with restricted permissions
Expand Down
Loading
Loading