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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ uv run runbookproof scan docs/

```text
runbookproof scan PATH [--format {text,json,sarif}] [-o OUTPUT] [--ignore-rule RULE_ID] [--config PATH]
runbookproof rules [--format {text,json}]
```

### Human-readable output
Expand Down Expand Up @@ -170,6 +171,23 @@ uv run runbookproof scan docs/ \

An explicitly selected configuration file must exist and contain valid TOML.

## Rule catalogue

List every built-in rule:

```bash
uv run runbookproof rules
```

Generate machine-readable output:

```bash
uv run runbookproof rules --format json
```

The complete reference is available in
[`docs/rules.md`](docs/rules.md).

## Built-in verification packs

| Pack | Command family |
Expand Down Expand Up @@ -315,7 +333,6 @@ git diff --check

- Finding suppression with documented justifications
- Custom and third-party verification packs
- Expanded rule documentation
- Reusable GitHub Action distribution
- First public package release

Expand Down
113 changes: 113 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# RunbookProof rule catalogue

This reference lists the built-in static-analysis rules.

| Rule ID | Verification pack | Description |
|---|---|---|
| `RBP-AWS-001` | `aws-cli` | AWS CLI TLS certificate verification is disabled |
| `RBP-AWS-002` | `aws-cli` | AWS CLI command contains a literal credential |
| `RBP-AWS-003` | `aws-cli` | AWS CLI command deletes or destroys resources |
| `RBP-AWS-004` | `aws-cli` | AWS resource safety controls are bypassed |
| `RBP-AWS-005` | `aws-cli` | AWS EC2 mutation has no dry-run permission check |
| `RBP-AWS-006` | `aws-cli` | AWS CLI command enables public S3 access |
| `RBP-AWS-007` | `aws-cli` | EC2 security-group ingress is open to the internet |
| `RBP-AWS-008` | `aws-cli` | AWS IAM command grants broad administrative access |
| `RBP-AWS-009` | `aws-cli` | AWS CLI command returns sensitive credential material |
| `RBP-AWS-010` | `aws-cli` | AWS CLI command uses an unencrypted HTTP endpoint |
| `RBP-AWS-011` | `aws-cli` | AWS security monitoring is disabled or deleted |
| `RBP-AWS-012` | `aws-cli` | AWS database resource is configured as public |
| `RBP-AWS-013` | `aws-cli` | AWS CLI request is sent without request signing |
| `RBP-BASH-001` | `bash` | Remote script is piped directly to a shell |
| `RBP-BASH-002` | `bash` | Command performs recursive deletion<br>Recursive deletion targets a critical path |
| `RBP-BASH-003` | `bash` | Command grants world-writable permissions |
| `RBP-BASH-004` | `bash` | Command requires elevated privileges |
| `RBP-BASH-005` | `bash` | Destructive command contains an unquoted variable |
| `RBP-BASH-006` | `bash` | Command may not be portable across POSIX shells |
| `RBP-DOCKER-001` | `docker` | Remote Docker daemon connection is not verified |
| `RBP-DOCKER-002` | `docker` | Container is granted unrestricted host privileges |
| `RBP-DOCKER-003` | `docker` | Container shares host namespaces |
| `RBP-DOCKER-004` | `docker` | Container mounts the Docker daemon socket |
| `RBP-DOCKER-005` | `docker` | Container mounts a sensitive host path |
| `RBP-DOCKER-006` | `docker` | Container explicitly runs as root |
| `RBP-DOCKER-007` | `docker` | Container image is not pinned by digest |
| `RBP-DOCKER-008` | `docker` | Container security controls are disabled |
| `RBP-DOCKER-009` | `docker` | Container receives dangerous Linux capabilities |
| `RBP-DOCKER-010` | `docker` | Finding generated by the docker verification pack. |
| `RBP-DOCKER-011` | `docker` | Command contains a literal secret value |
| `RBP-GIT-001` | `git` | Hard reset can permanently discard local changes |
| `RBP-GIT-002` | `git` | Forced push can overwrite remote history |
| `RBP-GIT-003` | `git` | Force-with-lease can rewrite remote history |
| `RBP-GIT-004` | `git` | Git clean can remove untracked directories or files<br>Git clean permanently removes untracked files |
| `RBP-GIT-005` | `git` | Forced branch deletion can remove unmerged work |
| `RBP-GIT-006` | `git` | Command can discard working-tree changes |
| `RBP-GIT-007` | `git` | Command deletes a remote Git branch |
| `RBP-GIT-008` | `git` | Git pull does not specify an integration strategy |
| `RBP-GIT-009` | `git` | Branch creation uses legacy checkout syntax |
| `RBP-GIT-010` | `git` | Command permanently removes Git stash entries |
| `RBP-KUBECTL-001` | `kubectl` | Kubernetes API certificate verification is disabled |
| `RBP-KUBECTL-002` | `kubectl` | kubectl command contains a literal credential |
| `RBP-KUBECTL-003` | `kubectl` | Mutation targets every Kubernetes namespace |
| `RBP-KUBECTL-004` | `kubectl` | kubectl command deletes Kubernetes resources |
| `RBP-KUBECTL-005` | `kubectl` | kubectl command bypasses graceful safety controls |
| `RBP-KUBECTL-006` | `kubectl` | Mutation selects all matching Kubernetes resources |
| `RBP-KUBECTL-007` | `kubectl` | kubectl mutation has no dry-run preview |
| `RBP-KUBECTL-008` | `kubectl` | Server-side apply forcibly overrides field conflicts |
| `RBP-KUBECTL-009` | `kubectl` | kubectl command opens an interactive cluster session |
| `RBP-KUBECTL-010` | `kubectl` | kubectl command contains a literal secret value |
| `RBP-KUBECTL-011` | `kubectl` | Kubernetes workload image is not pinned by digest |
| `RBP-KUBECTL-012` | `kubectl` | kubectl command creates privileged workload access |
| `RBP-KUBECTL-013` | `kubectl` | Kubernetes object schema validation is disabled |
| `RBP-KUBECTL-014` | `kubectl` | kubectl command accesses a raw Kubernetes API path |
| `RBP-NODE-001` | `node-package` | Package manager disables transport security |
| `RBP-NODE-002` | `node-package` | Executed package does not specify a version |
| `RBP-NODE-003` | `node-package` | Package execution suppresses confirmation |
| `RBP-NODE-004` | `node-package` | Command installs a package globally |
| `RBP-NODE-005` | `node-package` | Dependency safety checks are bypassed |
| `RBP-NODE-006` | `node-package` | Dependency installation may not be reproducible |
| `RBP-NODE-007` | `node-package` | Package lifecycle scripts are explicitly enabled |
| `RBP-NODE-008` | `node-package` | Package security checks are disabled |
| `RBP-NODE-009` | `node-package` | npm --save is redundant in modern npm |
| `RBP-PYTHON-001` | `python-package` | Python package transport security is disabled |
| `RBP-PYTHON-002` | `python-package` | Python package installation is not exactly pinned |
| `RBP-PYTHON-003` | `python-package` | Remote Python package source is not immutable |
| `RBP-PYTHON-004` | `python-package` | Command can modify a system-managed Python installation |
| `RBP-PYTHON-005` | `python-package` | Requirements installation does not require hashes |
| `RBP-PYTHON-006` | `python-package` | Package hash verification is disabled |
| `RBP-PYTHON-007` | `python-package` | uv sync may update the project lockfile |
| `RBP-PYTHON-008` | `python-package` | Command uses a legacy Python installation interface |
| `RBP-TERRAFORM-001` | `terraform` | Terraform changes are automatically approved without a saved plan |
| `RBP-TERRAFORM-002` | `terraform` | Terraform command destroys managed infrastructure |
| `RBP-TERRAFORM-003` | `terraform` | Terraform apply generates a new plan at execution time |
| `RBP-TERRAFORM-004` | `terraform` | Terraform operation uses targeted or forced resource scope |
| `RBP-TERRAFORM-005` | `terraform` | Terraform state locking is disabled |
| `RBP-TERRAFORM-006` | `terraform` | Terraform state lock is forcibly removed |
| `RBP-TERRAFORM-007` | `terraform` | Finding generated by the terraform verification pack. |
| `RBP-TERRAFORM-008` | `terraform` | Terraform state push disables remote state safety checks |
| `RBP-TERRAFORM-009` | `terraform` | Terraform command contains a literal secret value |
| `RBP-TERRAFORM-010` | `terraform` | Terraform command exposes potentially sensitive state or output data |
| `RBP-TERRAFORM-011` | `terraform` | Terraform refresh command is deprecated |
| `RBP-TERRAFORM-012` | `terraform` | Terraform initialization upgrades dependency selections |
| `RBP-TERRAFORM-013` | `terraform` | Terraform workspace is deleted |
| `RBP-TERRAFORM-014` | `terraform` | Terraform import modifies resource state directly |
| `RBP-UNIVERSAL-001` | `universal` | Command contains invalid shell syntax |
| `RBP-UNIVERSAL-002` | `universal` | Command has no detectable executable |
| `RBP-UNIVERSAL-010` | `universal` | Command uses a shell pipeline |
| `RBP-UNIVERSAL-011` | `universal` | Command uses conditional command chaining |
| `RBP-UNIVERSAL-012` | `universal` | Command contains sequential operations |
| `RBP-UNIVERSAL-013` | `universal` | Command starts a background operation |
| `RBP-UNIVERSAL-014` | `universal` | Command redirects shell input or output |
| `RBP-UNIVERSAL-015` | `universal` | Command performs command substitution |
| `RBP-UNIVERSAL-016` | `universal` | Command performs process substitution |
| `RBP-UNIVERSAL-017` | `universal` | Command performs arithmetic expansion |

Display the catalogue from the command line:

```bash
runbookproof rules
```

Generate machine-readable output:

```bash
runbookproof rules --format json
```
66 changes: 66 additions & 0 deletions src/runbookproof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
TerraformPack,
UniversalPack,
)
from runbookproof.rules import RULES, RuleInfo

OutputFormat = Literal["text", "json", "sarif"]
RulesFormat = Literal["text", "json"]
SarifLevel = Literal["error", "warning", "note"]

_SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
Expand Down Expand Up @@ -207,6 +209,19 @@ def build_parser() -> argparse.ArgumentParser:
),
)

rules_parser = subparsers.add_parser(
"rules",
help="List built-in static-analysis rules.",
description=("Display the built-in RunbookProof rule catalogue."),
)
rules_parser.add_argument(
"--format",
choices=("text", "json"),
default="text",
dest="output_format",
help="Output format. Defaults to text.",
)

return parser


Expand All @@ -226,6 +241,49 @@ def _built_in_packs() -> tuple[VerificationPack, ...]:
)


def _rule_to_dict(
rule: RuleInfo,
) -> dict[str, object]:
"""Convert one catalogue rule into JSON data."""
return {
"rule_id": rule.rule_id,
"pack": rule.pack_name,
"messages": list(rule.messages),
}


def _print_rule_catalog(
output_format: RulesFormat,
) -> None:
"""Print the built-in rule catalogue."""
if output_format == "json":
_print_json(
{
"rule_count": len(RULES),
"rules": [_rule_to_dict(rule) for rule in RULES],
}
)
return

rule_id_width = max(
len("RULE ID"),
*(len(rule.rule_id) for rule in RULES),
)
pack_width = max(
len("PACK"),
*(len(rule.pack_name) for rule in RULES),
)

print(f"{'RULE ID':<{rule_id_width}} {'PACK':<{pack_width}} DESCRIPTION")

for rule in RULES:
print(
f"{rule.rule_id:<{rule_id_width}} "
f"{rule.pack_name:<{pack_width}} "
f"{rule.description}"
)


def _pluralized(count: int, noun: str) -> str:
"""Return a count with a correctly pluralized noun."""
suffix = "" if count == 1 else "s"
Expand Down Expand Up @@ -719,6 +777,14 @@ def main(
parser.print_help()
return 0

if command == "rules":
rules_format = cast(
RulesFormat,
arguments.output_format,
)
_print_rule_catalog(rules_format)
return 0

if command == "scan":
path = cast(
Path,
Expand Down
Loading