Metadata-Version: 2.5
Name: afterlife-audit
Version: 0.3.1
Summary: Surface credentials that outlive their owners: a ghost-access auditor.
Project-URL: Homepage, https://github.com/desledishant10/afterlife
Project-URL: Repository, https://github.com/desledishant10/afterlife
Project-URL: Issues, https://github.com/desledishant10/afterlife/issues
Project-URL: Changelog, https://github.com/desledishant10/afterlife/blob/main/CHANGELOG.md
License-Expression: MIT
License-File: LICENSE
Keywords: audit,credentials,ghost-access,iam,offboarding,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Requires-Python: >=3.11
Requires-Dist: boto3>=1.34
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: jinja2>=3.1
Requires-Dist: networkx>=3.2
Requires-Dist: pyjwt[crypto]>=2.8
Requires-Dist: python-dateutil>=2.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn[standard]>=0.30
Provides-Extra: dev
Requires-Dist: freezegun>=1.5; extra == 'dev'
Requires-Dist: moto>=5.0; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: pdf
Requires-Dist: weasyprint>=62; extra == 'pdf'
Description-Content-Type: text/markdown

# Afterlife

[![CI](https://github.com/desledishant10/afterlife/actions/workflows/ci.yml/badge.svg)](https://github.com/desledishant10/afterlife/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

> Credentials that outlive their owners.

![Afterlife demo: 20 findings, 16 persons across 8 sources, cross-source identity graph](docs/media/demo.gif)

**Afterlife** is a ghost-access auditor. It pulls identities and credentials
from your cloud, code, IdP, and SaaS systems into one identity graph, then
runs detection rules over that graph and ranks findings by blast radius.

The class of credentials it surfaces drives a disproportionate share of modern
breaches: every API key generated by a contractor who left two years ago,
every OAuth grant tied to a deprovisioned employee, every long-lived AWS
access key on an account marked "suspended" in the IdP. Uber 2022, Okta 2023,
MOVEit, Snowflake 2024 all involve some version of this.

Most tools treat each system in isolation. Afterlife's value is the
**cross-source identity graph**: an AWS access key's "owner" might be active
in AWS but suspended in Google Workspace; without joining the two views, you
miss it.

## Status

`v0.3`. 9 source systems collected, 16 detection rules, 380+ tests.

## Source systems

| | |
|---|---|
| **Cloud** | AWS IAM, GCP IAM |
| **Code hosting** | GitHub, GitLab |
| **Identity providers** | Google Workspace, Microsoft Entra ID, Okta |
| **Operational** | Slack, HashiCorp Vault |

Plus a **CloudTrail** enrichment collector (`afterlife scan cloudtrail`): it adds
no identities, but reads recent CloudTrail events to attach observed last-use
and used-services to the AWS credentials, giving the usage-based rules
ground-truth data. Run it after `scan aws`.

Each collector lives in `src/afterlife/collectors/`. They are intentionally
dumb (no analysis), idempotent (re-run safe), and tested against mocked APIs
(no live calls in CI). Vault's `aliases` field is used to add cross-system
graph edges directly, even without shared email.

## What it detects

| Rule | Severity | What it catches |
|------|----------|-----------------|
| `OFFBOARDED-OWNER` | Critical | Active credential whose owner (or any cross-source linked identity) is suspended/archived/deleted in an IdP. The Uber-2022 pattern. |
| `CROSS-ACCOUNT-TRUST` | Critical | IAM role trusts an external AWS account. The Capital-One-2019 precondition. |
| `PUBLIC-ROLE-TRUST` | Critical | IAM role assumable by any AWS principal (wildcard `Principal`) with no restricting condition. |
| `ADMIN-CONCENTRATION` | Critical | Same person holds admin-tier access in 2+ systems (IdP admin flag + AWS AdministratorAccess + ...). |
| `ADMIN-WITHOUT-MFA` | Critical | IdP admin (Google Workspace today) without 2-step verification enforced. |
| `UNUSED-CREDENTIAL` | High | Active credential not used in N days (default 90). |
| `STALE-DEPLOY-KEY-WRITE` | High | Write-capable deploy key not used in N days. |
| `STALE-OAUTH` | High | Write-scoped third-party OAuth grant not used in N days (default 90). |
| `OUTSIDE-COLLAB-WITH-AWS` | High | GitHub outside collaborator linked to active AWS credentials. |
| `ORPHANED-GITHUB` | High | Active GitHub PAT whose owner is no longer in the org (Enterprise SAML). |
| `INACTIVE-ADMIN` | High | Admin who hasn't logged in within the inactivity window. |
| `UNROTATED-KEY` | Medium | Long-lived static cloud key (AWS / GCP) past the rotation threshold. |
| `PRIVILEGE-DRIFT` | Medium | IAM role granted far more AWS services than it uses (Access Advisor, refined by CloudTrail). |
| `USER-WITHOUT-MFA` | Medium | Active non-admin user (Google Workspace today) with no 2-step verification. The Snowflake-2024 pattern. |
| `NEVER-USED` | Medium | Active credential past the grace period with no usage record. |
| `ORPHANED-IDENTITY` | Low | IdP identity with no downstream system presence (hygiene signal). |

Each rule's logic, false-positive notes, and remediation are in
[docs/DETECTIONS.md](docs/DETECTIONS.md).

## Quickstart

Zero-config demo against in-memory mocks for eight of the nine source systems (all but Okta):

```bash
make install
make demo
```

The demo plants synthetic users, credentials, and IdP records across every
collector, runs them, and produces 20 deterministic findings with one
`OFFBOARDED-OWNER` (bob, broad blast) and one `ADMIN-CONCENTRATION` (dave is
admin in 3 systems). Identity graph: 16 persons across 8 sources, 6
cross-source. Demo also writes `.afterlife-demo-report.html` you can open in
a browser.

Against real systems:

```bash
make install
.venv/bin/afterlife init
.venv/bin/afterlife scan aws --profile my-profile
.venv/bin/afterlife scan cloudtrail --profile my-profile   # enrich AWS usage
.venv/bin/afterlife scan gcp --project my-project
.venv/bin/afterlife scan github --org my-org --token $GITHUB_TOKEN
.venv/bin/afterlife scan gitlab --group my-group --token $GITLAB_TOKEN
.venv/bin/afterlife scan idp --provider google     # or okta / azure
.venv/bin/afterlife scan slack --token $SLACK_TOKEN
.venv/bin/afterlife scan vault --api-url https://vault.example.com:8200
.venv/bin/afterlife analyze --allowlist allowlist.yaml
.venv/bin/afterlife identities
.venv/bin/afterlife report --format html -o report.html
.venv/bin/afterlife serve                          # localhost dashboard
```

See [.env.example](.env.example) for required environment variables.

## Reports

`afterlife report` emits four formats. Each carries the same finding set;
choose based on consumer.

| Format | Use it for |
|--------|------------|
| `json` | Programmatic consumption, scripting, pipelines |
| `html` | Self-contained audit handout, attach to a PR or email |
| `pdf` | Publication-ready handout for stakeholders (requires `[pdf]` extra and Pango) |
| `sarif` | GitHub Code Scanning, Azure DevOps, GitLab security feeds |

```bash
.venv/bin/afterlife report --format pdf -o audit.pdf
```

## Web dashboard

`afterlife serve` launches a local FastAPI dashboard with nine pages.

<p>
  <img alt="Overview: severity tiles, blast tiers, identity-graph stats" src="docs/media/overview.png" width="320">
  <img alt="Findings list with filter, sort, suppression toggle"        src="docs/media/findings.png" width="320">
  <img alt="Identity graph: alice 7-way cross-source, suspended/archived statuses surfaced in red" src="docs/media/identities.png" width="320">
</p>

<details>
<summary>More screenshots</summary>

| Credentials across all sources | Credentials, continued |
|---|---|
| <img alt="Credentials"     src="docs/media/credentials.png"      width="420"> | <img alt="Credentials, continued" src="docs/media/credentials-more.png" width="420"> |

| Findings (medium / low tiers, orphaned identities) | Overview, bottom (top findings by blast radius) |
|---|---|
| <img alt="Findings, more"  src="docs/media/findings-more.png"    width="420"> | <img alt="Overview, bottom" src="docs/media/overview-detail.png"      width="420"> |

| Identities, mixed-source middle | Identities, single-source bottom (bots, no-email cases) |
|---|---|
| <img alt="Identities, middle" src="docs/media/identities-middle.png" width="420"> | <img alt="Identities, bottom" src="docs/media/identities-bottom.png"  width="420"> |

Every scan run captured by the operational scan-history page:

<img alt="Scan history: 8 collectors run with status, records, duration" src="docs/media/scan-history.png" width="900">

</details>

The pages:

- **Overview**: severity tiles, blast-tier chart, last-scan-per-source.
- **Findings**: filterable, searchable, sortable, expandable evidence + remediation, one-click `ack` per finding (state in localStorage), HTMX-powered live filtering.
- **Trends**: finding history over time from the lifecycle timestamps: open findings by severity, new-vs-resolved flow, and headline stats (open now, seen ever, resolved, median days to resolve).
- **Credentials**: sortable table with source / type / active filters, click into per-credential detail.
- **Identities**: person-grouped, filterable to cross-source only, click into per-person detail showing all linked identities + owned credentials + active findings.
- **Finding / Credential / Person detail pages**: deep-linked, fully cross-referenced.
- **Scan history**: every `afterlife scan ...` run with start/end/duration/status.

The dashboard is **read-only**: no DB writes, no auth, hardened with a strict
CSP, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`,
`Cross-Origin-Opener-Policy: same-origin`, disabled OpenAPI/docs endpoints,
and self-hosted HTMX (no CDN). Dark mode follows
`prefers-color-scheme`. Keyboard shortcuts (`/` to search, `g h/f/c/i` to
navigate, `?` for help). Includes a print stylesheet for PDF-via-browser.

## Alerting

Afterlife tracks findings across runs (each `analyze` reports what is **new**,
**reopened**, or **resolved** since the last run), so it can alert you the
moment new ghost access appears rather than only when you go looking.

```bash
export AFTERLIFE_SLACK_WEBHOOK=https://hooks.slack.com/services/...
.venv/bin/afterlife analyze --notify        # alert on new/reopened findings
```

Channels (any combination, configured via environment variables, never
persisted):

| Channel | Configure with |
|---------|----------------|
| Slack   | `AFTERLIFE_SLACK_WEBHOOK` (Incoming Webhook URL), or `--slack-webhook` |
| Webhook | `AFTERLIFE_WEBHOOK_URL` (alerts POSTed as JSON), or `--webhook` |
| Email   | `AFTERLIFE_SMTP_HOST` + `AFTERLIFE_EMAIL_TO` (plus optional SMTP auth) |
| Jira **(Pro)** | `AFTERLIFE_JIRA_URL` + `_EMAIL` + `_TOKEN` + `_PROJECT` (files a remediation ticket per new finding) |

Only **new and reopened** findings at or above a severity threshold are sent
(`AFTERLIFE_NOTIFY_MIN_SEVERITY`, default `high`; or `--notify-min-severity`),
and suppressed findings are never alerted. Run it on a schedule (cron, the CI
workflow below) to turn Afterlife into a continuous monitor. See
[.env.example](.env.example) for every variable.

## Continuous monitoring

`afterlife run` executes the whole pipeline in one pass (scan the configured
sources, analyze, and with `--notify` alert), and `afterlife watch` repeats it
on an interval. Both self-initialize the database, read credentials from the
environment, and skip any source that is not configured, so one broken
credential never stops the monitor.

```bash
# one pass over whatever the environment has credentials for
.venv/bin/afterlife run --notify

# or run continuously, hourly, over an explicit source list
.venv/bin/afterlife watch --interval 3600 --notify -s aws -s github -s idp
```

Keep the source list and cadence declarative with a config file (see
[afterlife.example.yml](afterlife.example.yml)):

```bash
.venv/bin/afterlife watch --config afterlife.yml
```

### Docker

```bash
docker build -t afterlife .
docker run --rm -v afterlife-data:/data --env-file .env \
    afterlife watch --notify -s aws -s github
```

The image runs as a non-root user and keeps its database on the `/data`
volume; see the [Dockerfile](Dockerfile).

## CI integration

```yaml
# .github/workflows/afterlife.yml (excerpt)
- run: afterlife report --format sarif -o afterlife.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: afterlife.sarif
```

Full workflow at [.github/workflows/afterlife.yml](.github/workflows/afterlife.yml).
It assumes an AWS role via OIDC, scans every source the team uses, uploads
SARIF to Code Scanning, and saves an HTML report as a 30-day artifact.

## Allowlist / suppression

`afterlife analyze --allowlist allowlist.yaml` reads a YAML file naming
findings to suppress. Suppressed findings are persisted (still auditable) but
hidden from the default dashboard view. Example:

```yaml
- rule_id: NEVER-USED
  credential_id: arn:aws:iam::123:role/SeasonalReportingRole
  reason: Yearly audit role, intentionally dormant
  until: 2027-01-01
```

Matchers: `rule_id`, `credential_id`, `identity_source`, `identity_id`. All
named fields must match. Catch-all entries (no matchers) are refused at load
time.

## Editions

Afterlife is **open core**. Everything above is free and always will be:
detection, the identity graph, blast scoring, monitoring with history,
alerting, `run` / `watch`, reports, and the local dashboard.

**Pro** adds team/enterprise features, unlocked by an offline license key (a
signed token verified locally against an embedded public key -- no license
server, nothing phones home):

| Pro feature | |
|---|---|
| Dashboard authentication | `afterlife serve --require-auth` password-protects the dashboard so you can safely expose it to a team |
| Single sign-on (OIDC) | `afterlife serve --sso` puts the dashboard behind your identity provider (Google, Okta, Entra, Auth0, Keycloak) with an optional email/domain allow-list |
| Ticketing integrations | Files a Jira issue for new and reopened findings on `afterlife analyze --notify`, turning ghost access into tracked work |

**Get Pro** (from **$990/year**, founding rate, first 50 organizations, price
locked for life): email
[didesle7@gmail.com](mailto:didesle7@gmail.com?subject=Afterlife%20Pro%20license)
with your organization name and you'll get a signed license key to activate:

```bash
afterlife license                       # show your edition
export AFTERLIFE_LICENSE=<token>        # activate (or AFTERLIFE_LICENSE_FILE=<path>)
```

<sub>Maintainers mint licenses with `scripts/issue_license.py` using the
vendor private key, which is never committed.</sub>

## Architecture

```
  collectors/  ─►  SQLite  ─►  identity graph  ─►  rules engine  ─►  blast scoring  ─►  reports
   AWS / GCP                   (NetworkX,           (pluggable)        (per-finding)     (json/html
   GitHub / GitLab              email + Vault                                             /sarif/pdf)
   Google / Okta / Azure        aliases)
   Slack / Vault
```

Five layers, each with a narrow boundary. Collectors write to SQLite only.
Rules read from SQLite + identity graph only. Scoring is pure (input:
credential, output: blast radius). Reports are pure (input: DB, output:
text). The dashboard wraps the same readers behind FastAPI. Details in
[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).

## Project layout

```
src/afterlife/
├── collectors/    9 source collectors + a CloudTrail usage-enrichment pass
├── rules/         16 detection rules, one file each, decorator-registered
├── graph/         Identity graph (NetworkX), email + Vault-alias linking
├── scoring/       Blast-radius scoring with explainable factors
├── reporting/     JSON, HTML, SARIF, PDF
├── notify/        Alerting: Slack, webhook, email (SMTP)
├── web/           FastAPI dashboard + templates + static assets
├── runner.py      Pipeline: scan -> analyze -> notify (run / watch)
├── allowlist.py   YAML suppression loader + matcher
├── licensing.py   Offline Pro-license verification (open core)
├── scan_runs.py   Run-tracking context manager
├── db.py          SQLite schema + helpers
├── models.py      Identity, Credential, Finding, BlastRadius
└── cli.py         Typer CLI

tests/             380+ tests using moto, respx, freezegun, fastapi.testclient
demo/              Self-contained `make demo` (mocks for every collector)
docs/              ARCHITECTURE.md, DETECTIONS.md, INTERVIEW_TALK_TRACK.md
.github/workflows  Production-ready GitHub Action
```

## Further reading

- **[docs/PLAYBOOK.md](docs/PLAYBOOK.md): complete usage walkthrough (start here for hands-on)**
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): layered design + why these boundaries
- [docs/DETECTIONS.md](docs/DETECTIONS.md): every rule, false-positive notes, remediation
- [docs/blog/the-graph-layer.md](docs/blog/the-graph-layer.md): design essay on why a graph is the right shape for cross-source ghost-access detection
- [docs/INTERVIEW_TALK_TRACK.md](docs/INTERVIEW_TALK_TRACK.md): prepared narratives for portfolio conversations
- [docs/KEY-MANAGEMENT.md](docs/KEY-MANAGEMENT.md): backing up and rotating the license-signing key (maintainers)
- [CHANGELOG.md](CHANGELOG.md): full release + milestone history

## Why "Afterlife"

The credentials this tool finds shouldn't still be alive.

## License

MIT
