Skip to content
Merged
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Plus `scoring.py` (the 0-100 score) and `plexavo/report/ai_narration.py`

Illustrative example (a public S3 bucket, an unencrypted volume, a role that's
never been assumed) — this is what a scan actually surfaces, including a
finding walked through `--explain`:
finding's free template remediation (shown by default, no `--explain`
needed; `--explain` would replace this panel with a full AI narrative
instead):

```
Your AWS Security Score: 74/100 (Good)
Expand Down Expand Up @@ -154,25 +156,31 @@ plexavo scan --profile my-aws-profile --report-html report.html
```

No `--profile`? It uses your default profile / environment variables,
same resolution order as the AWS CLI. No AI, no API key, no cost — this
alone is a complete, genuinely useful scan.
same resolution order as the AWS CLI. No AI, no API key, no cost — and
findings still come with free Next Step / Full Fix Detail guidance
wherever a template exists (10 common check types); this alone is a
complete, genuinely useful scan.

Want plain-English explanations for each finding too (needs the
`[ai]` install above):
Want a full AI-written explanation for *every* finding instead (needs
the `[ai]` install above)?

```bash
export ANTHROPIC_API_KEY="sk-ant-..." # your own key, your own account
plexavo scan --profile my-aws-profile --explain --report-html report.html --report-pdf report.pdf
```

- Drop `--explain` for a fast, free scan with raw technical findings only.
- Drop `--explain` and findings still get free template remediation where
available (no API key needed) and raw technical detail otherwise.
`--explain` replaces that with a live AI narrative for every finding,
including the templated ones.
- Drop `--report-html`/`--report-pdf` to just see the console table.
- `--explain-limit N` (default 25) caps how many findings get AI narration
in one run, as a safety rail against unexpectedly large real scans.
- `--explain-limit N` (default 25) caps how many findings get a live AI
call when `--explain` is passed, as a safety rail against unexpectedly
large real scans — it doesn't limit the free template remediation.
- No `ANTHROPIC_API_KEY` set, or a call fails for any reason (invalid key,
rate limit, network issue)? The scan and report are completely
unaffected — you get raw finding detail instead of narration for that
finding, not an error. See [Cost](#cost).
unaffected — that finding falls back to template/raw detail instead of
an AI narrative, not an error. See [Cost](#cost).

### Alternative: pipx

Expand Down Expand Up @@ -260,10 +268,14 @@ docs/
## Cost

Detection is free (pure Python/boto3), always, regardless of anything
else in this section. AI narration only runs with `--explain`, and even
then: 10 of the most common, narratively-generic finding types are
hand-written templates with zero API cost; only genuinely account-specific
findings call Claude, typically $0.01-0.02 per finding depending on answer
else in this section. Every scan also gets free Next Step / Full Fix
Detail remediation wherever one of 10 hand-written templates matches the
finding type — no flag, no API key, zero cost, on by default.

Live AI only runs with `--explain`, and when it does it's used for
*every* finding, including the 10 templated ones (a deliberate choice —
"AI narration on" always means fully AI-written content, not a mix of
template and AI), typically $0.01-0.02 per finding depending on answer
length. A full scan with `--explain` on a real account is usually well
under a dollar. This is **your own** `ANTHROPIC_API_KEY`, in **your own**
Anthropic account — this project never sees your key, never embeds one of
Expand Down
2 changes: 1 addition & 1 deletion plexavo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
anyone else. See README.md for usage, or `plexavo scan --help`.
"""

__version__ = "0.1.2"
__version__ = "0.2.0"
66 changes: 66 additions & 0 deletions plexavo/aws_profile_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""plexavo/aws_profile_setup.py — writes a new named AWS profile to disk.

Used by the interactive "+ Configure new profile" flow. Writes in the
same layout `aws configure --profile NAME` itself produces:
~/.aws/credentials -> [NAME] aws_access_key_id / aws_secret_access_key
~/.aws/config -> [profile NAME] region
(bare [default] for the "default" profile)

Only the target profile's section is added/updated — every other
section in either file is read back untouched via configparser's
read-modify-write round trip. One known limitation: configparser does
not preserve comment lines on rewrite, so hand-written comments in
these files (uncommon, since they're normally CLI/tool-managed) would
be dropped. Actual key/value data for every other profile is preserved
exactly.

Respects AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE if set, same as
the AWS CLI and boto3 itself.
"""

from __future__ import annotations

import configparser
import os
from pathlib import Path


def credentials_path() -> Path:
return Path(os.environ.get("AWS_SHARED_CREDENTIALS_FILE", "~/.aws/credentials")).expanduser()


def config_path() -> Path:
return Path(os.environ.get("AWS_CONFIG_FILE", "~/.aws/config")).expanduser()


def _write_ini(path: Path, section: str, values: dict[str, str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
parser = configparser.ConfigParser()
if path.exists():
parser.read(path)
if not parser.has_section(section):
parser.add_section(section)
for key, value in values.items():
parser.set(section, key, value)
with open(path, "w", encoding="utf-8") as f:
parser.write(f)
try:
os.chmod(path, 0o600)
except OSError:
# Windows doesn't support POSIX file-mode bits the same way —
# best-effort only, same as the AWS CLI itself does here.
pass


def write_profile(name: str, access_key_id: str, secret_access_key: str, region: str) -> None:
"""Write/overwrite one named profile's credentials and region.

Never logs or returns the secret — callers must not print it either.
"""
_write_ini(credentials_path(), name, {
"aws_access_key_id": access_key_id,
"aws_secret_access_key": secret_access_key,
})

config_section = "default" if name == "default" else f"profile {name}"
_write_ini(config_path(), config_section, {"region": region})
Loading
Loading