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
8 changes: 0 additions & 8 deletions .env.example

This file was deleted.

1 change: 0 additions & 1 deletion .envrc
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
source .venv/bin/activate
dotenv_if_exists .env
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
pipx install commitizen

- name: Install dependencies
run: poetry install --no-interaction --with dev,build
run: poetry install --no-interaction --with dev

- name: Bump version and build collection
id: bump
Expand Down
47 changes: 33 additions & 14 deletions .github/workflows/consistency-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@ jobs:
with:
python-version: "3.12"

- name: Install Poetry
run: python -m pip install poetry

# consistency_check.py is stdlib-only; PyYAML just enables the extra YAML checks.
- name: Install checker dependencies
run: poetry install --only build --no-interaction --no-root
run: python -m pip install --upgrade pyyaml

- name: Fetch pull request base
run: git fetch origin "${{ github.base_ref }}"
Expand All @@ -49,17 +47,26 @@ jobs:
run: |
set +e
report_txt="consistency-report.txt"
poetry run python cisco_sccfm_scripts/consistency_check.py \
: > "$report_txt"
python cisco_sccfm_scripts/consistency_check.py \
--base "${{ github.base_ref }}" \
--fail-on-warning | tee "$report_txt"
exit_code=${PIPESTATUS[0]}
counts=$(python -c "import pathlib, re; text = pathlib.Path('$report_txt').read_text(encoding='utf-8'); match = re.search(r'^\\s*(\\d+) error\\(s\\)\\s+(\\d+) warning\\(s\\)\\s+across', text, re.MULTILINE); errors = int(match.group(1)) if match else 0; warnings = int(match.group(2)) if match else 0; print(f'{errors} {warnings} {errors + warnings}')")
counts=$(CONSISTENCY_REPORT="$report_txt" python -c '
import os, pathlib, re
text = pathlib.Path(os.environ["CONSISTENCY_REPORT"]).read_text(encoding="utf-8")
pattern = r"^\s*(\d+) error\(s\)\s+(\d+) warning\(s\)\s+across"
match = re.search(pattern, text, re.MULTILINE)
errors = int(match.group(1)) if match else 0
warnings = int(match.group(2)) if match else 0
print(f"{errors} {warnings} {errors + warnings}")
')
read -r error_count warning_count issue_count <<< "$counts"
{
echo "exit_code=$exit_code"
echo "error_count=$error_count"
echo "warning_count=$warning_count"
echo "issue_count=$issue_count"
echo "error_count=${error_count:-0}"
echo "warning_count=${warning_count:-0}"
echo "issue_count=${issue_count:-0}"
echo "report_path=$report_txt"
} >> "$GITHUB_OUTPUT"

Expand All @@ -69,9 +76,12 @@ jobs:
error_count="${{ steps.consistency.outputs.error_count }}"
warning_count="${{ steps.consistency.outputs.warning_count }}"
issue_count="${{ steps.consistency.outputs.issue_count }}"
report_txt="consistency-report.txt"
echo "### Consistency Check" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "$issue_count" = "0" ]; then
if [ -z "$issue_count" ]; then
echo "The checker did not run — see the failing step above." >> "$GITHUB_STEP_SUMMARY"
elif [ "$issue_count" = "0" ]; then
echo "No consistency issues found." >> "$GITHUB_STEP_SUMMARY"
else
echo "**$issue_count** consistency issue(s) found." >> "$GITHUB_STEP_SUMMARY"
Expand All @@ -82,7 +92,11 @@ jobs:
echo "<details><summary>Checker output</summary>" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo '```text' >> "$GITHUB_STEP_SUMMARY"
sed -n '1,200p' consistency-report.txt >> "$GITHUB_STEP_SUMMARY"
if [ -f "$report_txt" ]; then
sed -n '1,200p' "$report_txt" >> "$GITHUB_STEP_SUMMARY"
else
echo "No checker output captured." >> "$GITHUB_STEP_SUMMARY"
fi
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "</details>" >> "$GITHUB_STEP_SUMMARY"
fi
Expand All @@ -107,9 +121,9 @@ jobs:
const errorCount = Number(process.env.CONSISTENCY_ERROR_COUNT || "0");
const warningCount = Number(process.env.CONSISTENCY_WARNING_COUNT || "0");
const issueCount = Number(process.env.CONSISTENCY_ISSUE_COUNT || "0");
const reportPath = process.env.CONSISTENCY_REPORT_PATH;
const reportPath = process.env.CONSISTENCY_REPORT_PATH || "";
const isFork = process.env.CONSISTENCY_IS_FORK === "true";
const rawReport = fs.existsSync(reportPath)
const rawReport = reportPath && fs.existsSync(reportPath)
? fs.readFileSync(reportPath, "utf8").trim()
: "No checker output captured.";
const maxReportChars = 60000;
Expand Down Expand Up @@ -231,5 +245,10 @@ jobs:
- name: Fail if consistency issues were found
if: always() && steps.consistency.outputs.exit_code != '0'
run: |
echo "Consistency check failed with ${{ steps.consistency.outputs.issue_count }} issue(s)."
exit_code="${{ steps.consistency.outputs.exit_code }}"
if [ -z "$exit_code" ]; then
echo "Consistency checker did not run; see the failing step above."
else
echo "Consistency check failed with ${{ steps.consistency.outputs.issue_count }} issue(s)."
fi
exit 1
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ results/
/.vscode/
/.env
/.env.*
!/.env.example
/.tox/
/.eggs/
.poetry_cache/
Expand Down Expand Up @@ -48,3 +47,6 @@ results/
# Local scratch / dev command files
*.local.sh
*.local.txt

# Consistency checker report artifact
/consistency-report.txt
21 changes: 10 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ This repository ships skill files that document how to interact with the CLI and
source cisco_sccfm_scripts/activate.sh

# Configure credentials once
sccfm-cli configure --region us --api-token <YOUR_TOKEN>
sccfm-cli configure --region us # securely prompts for the token

# Check connectivity
sccfm-cli status
Expand All @@ -58,19 +58,18 @@ sccfm-cli status
sccfm-cli inventory devices list --format table

# Interactive developer menu (test, lint, format, build collection, etc.)
devkit
sccfm-cli-interactive
```

## Required environment variables
## Credential configuration

Copy `.env.example` to `.env` and fill in your values (loaded automatically by direnv):
The only SCCFM token configuration source is the named profile store:

```bash
export SCCFM_REGION=us # int | us | eu | apj | au | uae | in | ci
export SCCFM_API_TOKEN="..." # from SCCFM UI > Settings > API Tokens
sccfm-cli --profile default configure --region us
```

Credentials are also stored under `~/.sccfm-cli/` after running `sccfm-cli configure`. Override the path with `--config-path` or `SCCFM_CONFIG`.
Profiles are stored in `~/.sccfm-cli/config.json` with owner-only POSIX permissions or inherited Windows user-profile ACLs. Override the path with `--config-path` or `SCCFM_CONFIG`. Do not configure SCCFM tokens through `.env`, inline Ansible parameters, or Ansible Vault. Vault remains appropriate for Ansible-specific device secrets.

## Testing instructions

Expand Down Expand Up @@ -101,8 +100,8 @@ No MCP servers are currently configured for this project. Skill files under `ski
# Build and install locally
build-ansible-collection

# Set up tokens and vault
devkit # select "change-tokens"
# Configure or select profiles interactively
sccfm-cli-interactive

# Verify inventory plugin
ansible-inventory -i sccfm-ansible/examples/inventory.sccfm.yml --graph
Expand All @@ -122,7 +121,7 @@ Add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` so IDE/mypy resolves `ansible_
git cz # or: ./cisco_sccfm_scripts/cz.sh commit
```
CI will fail on non-compliant commit messages.
- **Security**: Never commit real credentials, tokens, or secrets. Use placeholders and document required env vars. See [SECURITY.md](SECURITY.md) for vulnerability reporting.
- **Security**: Never commit real credentials, tokens, or secrets. Use placeholders and document the canonical profile flow. See [SECURITY.md](SECURITY.md) for vulnerability reporting.
- New commands go in `cisco_sccfm_cli/commands/` as a `BaseCommand` subclass, registered in `cisco_sccfm_cli/cli.py`.
- New SDK integrations go in `cisco_sccfm_core/services/`.
- Every behavior change must be accompanied by tests.
Expand All @@ -138,5 +137,5 @@ Add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` so IDE/mypy resolves `ansible_
#
# SPDX-License-Identifier: Apache-2.0
```
- **Secrets**: never read or commit `.env`, `.env.*`, `.vault_pass`, or real `vault.yml` files — use the `*.example` templates. Keep tracked `.envrc` files secret-free. `gitleaks` and `detect-private-key` block secrets in pre-commit.
- **Secrets**: never read or commit `.vault_pass`, real `vault.yml` files, or SCCFM profile files. Use the `*.example` vault templates for Ansible-specific secrets. Keep tracked `.envrc` files secret-free. `gitleaks` and `detect-private-key` block secrets in pre-commit.
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contribution guide.
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ reserve breaking changes until the next major version release.
direnv allow
```

3. Set up your SCCFM credentials:
3. Set up your SCCFM profile:
```bash
cp .env.example .env
# Edit .env with your API token
sccfm-cli configure --region us # securely prompts for the token
```

Now whenever you `cd` into the project, the virtualenv activates and env vars load automatically.
Now whenever you `cd` into the project, the virtualenv activates automatically.

## Committing Changes

Expand Down
26 changes: 11 additions & 15 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,14 @@ After sourcing, tab completion will work for all `sccfm` commands and options.
The same PyPI package exposes the typed `cisco_sccfm_core` library for Python automation:

```python
from dataclasses import dataclass

from cisco_sccfm_core import InventoryService
from cisco_sccfm_core.services import ProfileService

profile = ProfileService().load("default")
if profile is None:
raise RuntimeError("Configure the default profile with sccfm-cli configure")

@dataclass(frozen=True)
class Config:
region: str
api_token: str


inventory = InventoryService(Config(region="us", api_token="..."))
inventory = InventoryService(profile)
devices = inventory.get_devices(limit=10, offset=0, query=None)
```

Expand Down Expand Up @@ -174,19 +170,19 @@ ansible-galaxy collection list | grep cisco.sccfm

### Try out examples

The fastest way to get going is to use the interactive devkit menu:
The fastest way to get going is to use the interactive CLI menu:

```bash
devkit
# select "change-tokens" from the menu
sccfm-cli-interactive
# select "configure-profile" from the menu
```

Or run the token setup directly:
Or configure the canonical profile directly:

```bash
change-tokens
sccfm-cli configure --region us # securely prompts for the token
```

This will prompt for your region, API token, and vault password, then create all the required files (.env, vars.yml, vault.yml).
The profile is shared by `sccfm-cli`, `sccfm-cli-interactive`, and the `cisco.sccfm` Ansible collection. Ansible Vault remains available separately for managed-device passwords and other playbook-specific secrets.

See the [Trying out examples](sccfm-ansible/README.md#trying-out-examples) section in the Ansible collection README for the full walkthrough including how to run playbooks.
33 changes: 17 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ Python scripts, and collection can reuse the same SDK integrations.
cisco_sccfm_scripts/setup_environment.sh # installs pyenv, Python 3.12.4, Poetry deps
source cisco_sccfm_scripts/activate.sh # activates the project virtualenv
sccfm-cli --help # the main SCCFM CLI
devkit # interactive developer toolkit menu
sccfm-cli-interactive # interactive CLI and developer workflow menu
```

`setup_environment.sh` keeps everything local to the repository: pyenv provides Python 3.12.4, `.venv/` hosts the runtime, and Poetry installs the project plus dev dependencies.

## Commands

- `sccfm-cli configure [--region REGION] [--api-token TOKEN] [--config-path PATH]`: Captures the SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus an API token (see the [auth guide](https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/authentication/)) and stores it under `~/.sccfm-cli/` (override with `--config-path` or `SCCFM_CONFIG`).
- `sccfm-cli configure [--region REGION] [--api-token TOKEN] [--config-path PATH]`: Captures the SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus an API token (see the [auth guide](https://developer.cisco.com/docs/cisco-security-cloud-control-firewall-manager/authentication/)) in the canonical profile store at `~/.sccfm-cli/config.json`. On POSIX systems, the directory is restricted to the current user (`0700`) and the file to owner read/write (`0600`). On Windows, the store inherits the user's profile-directory ACLs. Override the path with `--config-path` or `SCCFM_CONFIG`.
- `sccfm-cli status [--config-path PATH]`: Shows the current profile plus SCCFM connectivity health using Rich tables.
- `sccfm-cli inventory devices list [--limit N] [--offset N] [--query TEXT] [--format table|json]`: Lists device inventory with pagination and optional name filtering.
- `sccfm-cli inventory manager list [--limit N] [--offset N] [--query TEXT] [--format table|json]`: Lists manager inventory with the same filters.
Expand Down Expand Up @@ -69,18 +69,14 @@ Installing the `cisco-sccfm-devkit` package also exposes `cisco_sccfm_core`, a t
Python automation library built on top of the generated `scc-firewall-manager-sdk`.

```python
from dataclasses import dataclass

from cisco_sccfm_core import InventoryService
from cisco_sccfm_core.services import ProfileService

profile = ProfileService().load("default")
if profile is None:
raise RuntimeError("Configure the default profile with sccfm-cli configure")

@dataclass(frozen=True)
class Config:
region: str
api_token: str


inventory = InventoryService(Config(region="us", api_token="..."))
inventory = InventoryService(profile)
devices = inventory.get_devices(limit=10, offset=0, query=None)
```

Expand All @@ -91,27 +87,32 @@ The package root exports the supported public service classes and response model

- macOS: `brew install ansible` (this includes `ansible-galaxy`; verify with `ansible-galaxy --version`).
- Build and install the collection locally: `build-ansible-collection`.
- Set up tokens interactively: `devkit` and select **change-tokens** (saves your API token, creates `.env`, `.vault_pass`, encrypts `group_vars/all/vault.yml`, and sets the region).
- Configure profiles interactively: run `sccfm-cli-interactive` and select **configure-profile**.
- For IDEs/mypy, add `sccfm-ansible` to `ANSIBLE_COLLECTIONS_PATH` (or mark it as a source root) so imports under `ansible_collections.cisco.sccfm` resolve without installing.
- Configure SCCFM region (`int`, `us`, `eu`, `apj`, `au`, `uae`, `in`, or `ci`) plus `SCCFM_API_TOKEN`; you can set them via env vars or inline (i.e., write the values directly in the inventory file—useful for local dev, but prefer env vars or Ansible Vault for anything shared).
- Ansible modules and inventory select the same named SCCFM profile; they do not duplicate its region or API token in environment variables, playbooks, or Ansible Vault.
- Keep Ansible Vault for playbook-specific secrets such as managed-device passwords.
- Point Ansible at an inventory file that uses the plugin, e.g. `ansible-inventory -i sccfm-ansible/examples/inventory.sccfm.yml --graph`.
- A starter playbook is in `sccfm-ansible/examples/show_devices.yml`; it runs against the SCCFM devices discovered by the inventory plugin.
- Generated Ansible reference docs can be previewed locally with `generate-ansible-docs`; see [docs/README.md](docs/README.md) for details.

## Development

All common development tasks are available through the interactive `devkit` menu:
All common development tasks are available through the interactive CLI menu:

```bash
source cisco_sccfm_scripts/activate.sh
devkit
sccfm-cli-interactive
```

This presents an interactive selector with the following tasks:

| Task | Description |
|------|-------------|
| **change-tokens** | Set up SCCFM API tokens, .env, and Ansible Vault |
| **configure-profile** | Create or replace a canonical SCCFM profile |
| **manage-profiles** | Update or remove SCCFM profiles |
| **import-legacy-vault** | Copy profiles from the former vault token store without modifying it |
| **run-cli** | Discover and run an `sccfm-cli` command interactively |
| **run-ansible** | Select and run an example playbook |
| **build-collection** | Build the cisco.sccfm Ansible collection tarball |
| **generate-ansible-docs** | Generate Ansible reference docs from ansible-doc output |
| **generate-cli-docs** | Generate CLI reference docs from Click help output |
Expand Down
2 changes: 2 additions & 0 deletions cisco_sccfm_cli/commands/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def build_params(self) -> Sequence[click.Parameter]:
help="API token for the chosen region",
group=credential_group,
required=True,
prompt="API token",
hide_input=True,
),
]

Expand Down
22 changes: 22 additions & 0 deletions cisco_sccfm_cli/commands/tests/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,25 @@ def test_should_normalize_legacy_region_aliases(cli_runner: CliRunner, config_pa
stored = ConfigService(path=config_path).load("lab")
assert stored is not None
assert stored.region == "au"


def test_should_prompt_for_token_without_echoing_it(
cli_runner: CliRunner, config_path: Path
) -> None:
result = cli_runner.invoke(
cli,
[
"configure",
"--region",
"us",
"--config-path",
str(config_path),
],
input="prompted-secret\n",
)

assert result.exit_code == 0
assert "prompted-secret" not in result.output
stored = ConfigService(path=config_path).load("default")
assert stored is not None
assert stored.api_token == "prompted-secret"
4 changes: 4 additions & 0 deletions cisco_sccfm_cli/commands/tests/test_sccfm_cli_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ def test_cisco_sccfm_cli_skill_should_cover_schema_driven_operation() -> None:
for fragment in expected_fragments:
assert fragment in body

assert "sccfm-cli-interactive" in body
assert "SCCFM_API_TOKEN" not in body
assert "SCCFM_REGION" not in body


def test_cisco_sccfm_cli_skill_should_reference_fields_emitted_by_schema(
cli_runner: CliRunner,
Expand Down
Loading