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
44 changes: 21 additions & 23 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
<!--
Sync Impact Report
==================
Version change: 2.1.0 → 2.2.0
Rationale: Enacted as a prerequisite of spec 006 (inventory-driven vulnerability check via the
Cisco PSIRT API). Four changes, none of which removes or redefines a principle, so the
bump is MINOR. (1) Principle IV now states the *substance* of the undecided result —
it must be labelled undecided and never guessed — while allowing each mode the clearest
label for its own report; `Indeterminate` (advisory-driven) and `Needs review`
(inventory-driven) are both conforming. The same principle now also makes explicit what
was previously only implied: a "not affected" determination MUST rest on positive
confirmation, not on the absence of a result. (2) Source of truth admits Cisco's PSIRT
openVuln advisory API alongside ERP pages and CSAF JSON. (3) Networking admits
`apix.cisco.com` and `id.cisco.com`, scoped, with an explicit note on what is sent.
(4) Inputs/outputs records the `impact_report_*.html` report the new mode writes.
This constitution's own version is independent of the product version.

Modified principles:
- IV. Conservative & Traceable Analysis (undecided label is mode-appropriate but mandatory;
"not affected" now explicitly requires positive confirmation; PSIRT API added as traceable
source data; AI-normalization clause scoped to modes that use AI)
Version change: 2.2.0 → 2.2.1
Rationale: The GitHub repository was renamed `xavient/cisco-advisory-impact-agent` →
`xavient/caia-cli` and the distribution renamed `cisco-advisory-impact-agent` →
`caia-cli`, aligning both with the `caia` command the tool has always installed. The
only change here is the example install command in the Distribution constraint, which
must stay accurate per the Documentation gate. No principle, rule, or gate changes in
substance, so the bump is PATCH. The product name ("Cisco Advisory Impact Agent") is
unchanged — `caia-cli` names the artifact, not the product. This constitution's own
version is independent of the product version.

Modified principles: (none)
Added sections: (none)
Modified sections:
- Technology & Data Constraints (Source of truth, Networking, Inputs/outputs bullets)
- Technology & Data Constraints (Distribution bullet: install command reflects the new
distribution name)
Removed sections: (none)

Templates requiring updates:
- .specify/templates/plan-template.md ✅ aligned (Constitution Check resolves gates dynamically)
- .specify/templates/spec-template.md ✅ aligned (no constitution references)
- .specify/templates/tasks-template.md ✅ aligned (no constitution references)

Follow-up TODOs: README.md and docs/index.html must document the new mode, its Cisco credential
prerequisite, and its HTML output before spec 006 merges (Documentation quality gate).
Follow-up TODOs: (none)

Previous version 2.2.0 (2026-07-25): enacted as a prerequisite of spec 006 (inventory-driven
vulnerability check via the Cisco PSIRT API). Principle IV made the undecided label mode-appropriate
but mandatory and required positive confirmation for "not affected"; Technology & Data Constraints
admitted the PSIRT openVuln API as a source of truth, `apix.cisco.com`/`id.cisco.com` as scoped
network destinations, and the `impact_report_*.html` output. MINOR.
Previous version 2.1.0 (2026-07-18): relaxed Principle II so a `uv` tool install is the primary,
recommended distribution rather than the sole one, adding `pip install .` from a source checkout
as a supported fallback; Technology & Data Constraints Distribution bullet updated to match. MINOR.
Expand Down Expand Up @@ -132,7 +130,7 @@ a personal credential; leaking either is a direct harm.

- Language/runtime: Python 3.9 or newer.
- Distribution: the recommended path installs, runs, and updates the tool as a `uv` tool
(`uv tool install cisco-advisory-impact-agent --from git+<repo>`); `uv` is an external
(`uv tool install caia-cli --from git+<repo>`); `uv` is an external
prerequisite on the user's machine, not a bundled runtime dependency. A `pip install .` from a
source checkout (into a virtual environment) is a supported fallback where `uv` is unavailable;
`--update` is uv-specific and does not apply on that path. The product version is single-sourced
Expand Down Expand Up @@ -225,4 +223,4 @@ Compliance is verified at review time. Dependent Spec Kit artifacts (plan, spec,
tasks templates and their generated documents) MUST remain consistent with the principles
above; the Constitution Check in the planning workflow enforces this on each feature.

**Version**: 2.2.0 | **Ratified**: 2026-07-14 | **Last Amended**: 2026-07-25
**Version**: 2.2.1 | **Ratified**: 2026-07-14 | **Last Amended**: 2026-07-25
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,24 @@ run the tool from; the report is written to an `output/` folder next to it.
With `uv` on your machine, install the tool straight from GitHub:

```bash
uv tool install cisco-advisory-impact-agent --from git+https://github.com/xavient/cisco-advisory-impact-agent
uv tool install caia-cli --from git+https://github.com/xavient/caia-cli
```

This puts a single **`caia`** command on your PATH. There is no
repository to clone and no virtual environment to create or activate.

> **Upgrading from the old name?** Before version 3.0.0 the tool installed as
> `cisco-advisory-impact-agent`. Both own the `caia` command, so remove the old one first — then
> run the install above:
>
> ```bash
> uv tool uninstall cisco-advisory-impact-agent
> ```
>
> Your saved credentials are stored outside the tool and are **not** touched by this, so there's
> no need to re-run `--config`. Note that `caia --update` cannot make this jump for you: the
> distribution name changed, so an old install has to be replaced with the command above.

Then set your FueliX API key once (stored per-user, used from any folder):

```bash
Expand Down Expand Up @@ -177,7 +189,7 @@ on it behaves exactly like the uv install — use it as described in [Run it](#r
- **Activate first, every session.** In each new terminal, re-run the activation command
(`source .venv/bin/activate` on macOS/Linux, `.venv\Scripts\Activate.ps1` on Windows) before
using the command. If you don't want to activate, call it by full path — e.g.
`.venv/bin/caia` — or run `python -m cisco_advisory_impact_agent.cli`.
`.venv/bin/caia` — or run `python -m caia.cli`.
- **Updating is manual here.** `--update` is uv-specific and will report that uv wasn't found. To
update, download the newer ZIP and re-run `python -m pip install .` in your environment. Pass
`--no-update-check` to skip the start-of-run version check.
Expand Down Expand Up @@ -323,8 +335,8 @@ delete that file yourself if you want it gone.

- `--uninstall` is uv-specific. If you installed [without uv](#install-without-uv-python-only), it
reports there's nothing to uninstall — remove it with the Python tooling you used
(e.g. `pip uninstall cisco-advisory-impact-agent`).
- If uv can't be found, it prints the exact manual command: `uv tool uninstall cisco-advisory-impact-agent`.
(e.g. `pip uninstall caia-cli`).
- If uv can't be found, it prints the exact manual command: `uv tool uninstall caia-cli`.
- On Windows, if the running command can't remove itself, close the terminal and run that manual
command from a fresh shell.
- The exit status is script-friendly: `0` once the tool is gone (including when it was already not
Expand Down Expand Up @@ -444,7 +456,7 @@ to suppress it everywhere.
<details>
<summary>How it works & how to hand it out</summary>

**Package layout** (`cisco_advisory_impact_agent/`)
**Package layout** (`caia/`)

| Module | Responsibility |
| ------ | -------------- |
Expand All @@ -466,6 +478,6 @@ combos are impacted and the tool expands them back into firewall names.
**Handing it to teammates:** point them at the install command above. Each teammate supplies
their own API key (`--config`) and inventory.

**Local development:** install from a checkout with `uv tool install --from . cisco-advisory-impact-agent --force`, and run the tests with `python -m unittest discover -s tests`. See `CONTRIBUTING.md` for the release process.
**Local development:** install from a checkout with `uv tool install --from . caia-cli --force`, and run the tests with `python -m unittest discover -s tests`. See `CONTRIBUTING.md` for the release process.

</details>
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.3.1
3.0.0
4 changes: 2 additions & 2 deletions cisco_advisory_impact_agent/__init__.py → caia/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Cisco Advisory Impact Agent — AI-driven Cisco firewall advisory impact analysis.

Distributed as a uv tool; the on-PATH command is `caia`
(entry point: :func:`cisco_advisory_impact_agent.cli.main`).
(entry point: :func:`caia.cli.main`).
"""

from __future__ import annotations

from cisco_advisory_impact_agent.version import read_installed_version
from caia.version import read_installed_version

__all__ = ["__version__"]

Expand Down
10 changes: 5 additions & 5 deletions cisco_advisory_impact_agent/analyzer.py → caia/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
which firewalls in your inventory are impacted. Writes a timestamped Excel report.

This module holds the analysis run flow; the command-line entry point is
`cisco_advisory_impact_agent.cli`. Inputs are read from, and the report is written
`caia.cli`. Inputs are read from, and the report is written
to, the current working folder (an `output/` subfolder), so the tool works from any
folder once installed.
"""
Expand All @@ -22,10 +22,10 @@
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from cisco_advisory_impact_agent import cisco, config, fuelix, html_report, matching, psirt
from cisco_advisory_impact_agent import inventory as inv
from cisco_advisory_impact_agent import ui
from cisco_advisory_impact_agent.report import write_report
from caia import cisco, config, fuelix, html_report, matching, psirt
from caia import inventory as inv
from caia import ui
from caia.report import write_report

CISCO_HOST = "sec.cloudapps.cisco.com"

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion cisco_advisory_impact_agent/cli.py → caia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import os
import sys

from cisco_advisory_impact_agent import analyzer, config, ui, version
from caia import analyzer, config, ui, version


class _Parser(argparse.ArgumentParser):
Expand Down
4 changes: 2 additions & 2 deletions cisco_advisory_impact_agent/config.py → caia/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys
from pathlib import Path

from cisco_advisory_impact_agent import fuelix, ui
from caia import fuelix, ui

APP_DIR_NAME = "caia"
CONFIG_FILE_NAME = "config"
Expand Down Expand Up @@ -193,7 +193,7 @@ def _verify_cisco(client_id, client_secret, interactive):

No message here contains any part of a credential (FR-049).
"""
from cisco_advisory_impact_agent import psirt
from caia import psirt

ui.plain()
ui.system("Checking the Cisco credentials ...")
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import html
from pathlib import Path

from cisco_advisory_impact_agent import matching as m
from caia import matching as m

FILENAME_PREFIX = "impact_report_"

Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion cisco_advisory_impact_agent/psirt.py → caia/psirt.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import urllib.parse
import urllib.request

from cisco_advisory_impact_agent.matching import (
from caia.matching import (
HAS_ADVISORIES,
INCOMPLETE,
INVALID_VERSION,
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions cisco_advisory_impact_agent/version.py → caia/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from importlib.metadata import PackageNotFoundError, version as _dist_version
from pathlib import Path

DIST_NAME = "cisco-advisory-impact-agent"
DEFAULT_REPO = "xavient/cisco-advisory-impact-agent" # owner/name of the GitHub repo for release + version checks
UA = {"User-Agent": "cisco-advisory-impact-agent"}
DIST_NAME = "caia-cli"
DEFAULT_REPO = "xavient/caia-cli" # owner/name of the GitHub repo for release + version checks
UA = {"User-Agent": "caia-cli"}

# VERSION lives at the repo root, one level above this package (used only for source-tree runs).
_VERSION_FILE = Path(__file__).resolve().parent.parent / "VERSION"
Expand Down
21 changes: 14 additions & 7 deletions docs/index.html

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["setuptools>=77.0.0"]
build-backend = "setuptools.build_meta"

[project]
name = "cisco-advisory-impact-agent"
name = "caia-cli"
description = "Agentic firewall impact analysis for Cisco security advisories"
readme = "README.md"
requires-python = ">=3.9"
Expand All @@ -17,11 +17,11 @@ dependencies = [
dynamic = ["version"]

[project.scripts]
caia = "cisco_advisory_impact_agent.cli:main"
caia = "caia.cli:main"

[project.urls]
Homepage = "https://github.com/xavient/cisco-advisory-impact-agent"
Repository = "https://github.com/xavient/cisco-advisory-impact-agent"
Homepage = "https://github.com/xavient/caia-cli"
Repository = "https://github.com/xavient/caia-cli"

# The committed VERSION file is the single source of truth for the product version
# (constitution "Versioning & releases" gate). setuptools reads its raw contents here and
Expand All @@ -31,4 +31,4 @@ Repository = "https://github.com/xavient/cisco-advisory-impact-agent"
version = { file = "VERSION" }

[tool.setuptools]
packages = ["cisco_advisory_impact_agent"]
packages = ["caia"]
6 changes: 3 additions & 3 deletions tests/test_api_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
from pathlib import Path
from unittest import mock

from cisco_advisory_impact_agent import analyzer
from cisco_advisory_impact_agent import matching as m
from cisco_advisory_impact_agent import psirt
from caia import analyzer
from caia import matching as m
from caia import psirt
from tests.fixtures import psirt_responses as fx
from tests.fixtures.inventory import ROWS, usable_row_count, write_fixture_inventory

Expand Down
2 changes: 1 addition & 1 deletion tests/test_banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from cisco_advisory_impact_agent import ui # noqa: E402
from caia import ui # noqa: E402


class BannerContentTests(unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from cisco_advisory_impact_agent import cli, version # noqa: E402
from caia import cli, version # noqa: E402


class HelpTests(unittest.TestCase):
Expand Down
16 changes: 8 additions & 8 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from cisco_advisory_impact_agent import config, fuelix # noqa: E402
from caia import config, fuelix # noqa: E402


class ConfigBaseDirTests(unittest.TestCase):
Expand Down Expand Up @@ -196,7 +196,7 @@ def fake_fetch_token(client_id, client_secret, **_kw):
raise token
return token or "a-token"

from cisco_advisory_impact_agent import psirt
from caia import psirt

with mock.patch.object(config, "config_dir", return_value=self.cfg_dir), \
mock.patch.object(config, "config_path", return_value=self.cfg_path), \
Expand Down Expand Up @@ -290,7 +290,7 @@ def test_valid_credentials_are_reported_as_valid(self):
self.assertIn("accepted", out.lower())

def test_rejected_credentials_show_ciscos_reason_and_are_not_saved(self):
from cisco_advisory_impact_agent import psirt
from caia import psirt
err = psirt.PsirtAuthError("Cisco rejected the credentials (HTTP 401): "
"invalid_client")
with self._session(visible=["bad-id"], secrets=["k", "bad-secret"],
Expand All @@ -304,7 +304,7 @@ def test_rejected_credentials_show_ciscos_reason_and_are_not_saved(self):
self.assertNotIn(config.CISCO_CLIENT_ID, saved)

def test_rejection_preserves_previously_saved_cisco_values(self):
from cisco_advisory_impact_agent import psirt
from caia import psirt
existing = {config.CISCO_CLIENT_ID: "good-old-id",
config.CISCO_CLIENT_SECRET: "good-old-secret"}
with self._session(visible=["new-bad-id"], secrets=["k", "new-bad-secret"],
Expand All @@ -317,7 +317,7 @@ def test_rejection_preserves_previously_saved_cisco_values(self):

def test_unreachable_is_distinct_from_rejected_and_can_be_saved(self):
"""FR-043: a proxy outage must never block configuration."""
from cisco_advisory_impact_agent import psirt
from caia import psirt
err = psirt.PsirtUnreachableError("Cisco could not be reached (URLError). "
"This is a connectivity problem, not a "
"credential problem.")
Expand All @@ -330,7 +330,7 @@ def test_unreachable_is_distinct_from_rejected_and_can_be_saved(self):
self.assertEqual(saved[config.CISCO_CLIENT_SECRET], "s")

def test_unreachable_declined_does_not_save(self):
from cisco_advisory_impact_agent import psirt
from caia import psirt
with self._session(visible=["id"], secrets=["k", "s"],
token=psirt.PsirtUnreachableError("offline"),
confirm=False) as (code, _out, _t):
Expand All @@ -339,7 +339,7 @@ def test_unreachable_declined_does_not_save(self):
self.assertNotIn(config.CISCO_CLIENT_SECRET, saved)

def test_unreachable_in_a_non_interactive_session_saves_without_prompting(self):
from cisco_advisory_impact_agent import psirt
from caia import psirt
with self._session(visible=["id"], secrets=["k", "s"],
token=psirt.PsirtUnreachableError("offline"),
isatty=False) as (code, _out, _t):
Expand All @@ -348,7 +348,7 @@ def test_unreachable_in_a_non_interactive_session_saves_without_prompting(self):

# --- hygiene ---------------------------------------------------------- #
def test_no_credential_fragment_in_any_verification_message(self):
from cisco_advisory_impact_agent import psirt
from caia import psirt
secret = "leak-me-if-you-can"
outcomes = [None,
psirt.PsirtAuthError("rejected"),
Expand Down
2 changes: 1 addition & 1 deletion tests/test_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from cisco_advisory_impact_agent import cisco # noqa: E402
from caia import cisco # noqa: E402

DEFAULT_SAMPLES = [
"cisco-sa-onprem-fmc-authbypass-5JPp45V2.json", # relationships shape, FMC
Expand Down
4 changes: 2 additions & 2 deletions tests/test_folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from cisco_advisory_impact_agent import analyzer, ui # noqa: E402
from cisco_advisory_impact_agent import report # noqa: E402
from caia import analyzer, ui # noqa: E402
from caia import report # noqa: E402


def _make_inventory(path, rows=1):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_html_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import unittest
from pathlib import Path

from cisco_advisory_impact_agent import html_report as hr
from cisco_advisory_impact_agent import matching as m
from caia import html_report as hr
from caia import matching as m
from tests.fixtures import psirt_responses as fx


Expand Down
Loading
Loading