posturanet 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- posturanet-0.1.0/.gitignore +18 -0
- posturanet-0.1.0/CHANGELOG.md +62 -0
- posturanet-0.1.0/LICENSE.md +105 -0
- posturanet-0.1.0/PKG-INFO +85 -0
- posturanet-0.1.0/README.md +44 -0
- posturanet-0.1.0/integrations/README.md +75 -0
- posturanet-0.1.0/pyproject.toml +81 -0
- posturanet-0.1.0/sandbox/README.md +63 -0
- posturanet-0.1.0/src/posturanet/__init__.py +3 -0
- posturanet-0.1.0/src/posturanet/__main__.py +3 -0
- posturanet-0.1.0/src/posturanet/aws/__init__.py +3 -0
- posturanet-0.1.0/src/posturanet/aws/readonly.py +96 -0
- posturanet-0.1.0/src/posturanet/aws/session.py +55 -0
- posturanet-0.1.0/src/posturanet/cli/__init__.py +0 -0
- posturanet-0.1.0/src/posturanet/cli/app.py +283 -0
- posturanet-0.1.0/src/posturanet/contracts/__init__.py +18 -0
- posturanet-0.1.0/src/posturanet/contracts/builder.py +63 -0
- posturanet-0.1.0/src/posturanet/contracts/invariants.py +7 -0
- posturanet-0.1.0/src/posturanet/contracts/store.py +135 -0
- posturanet-0.1.0/src/posturanet/deliver/__init__.py +4 -0
- posturanet-0.1.0/src/posturanet/deliver/checks.py +98 -0
- posturanet-0.1.0/src/posturanet/deliver/github.py +231 -0
- posturanet-0.1.0/src/posturanet/deliver/idempotency.py +49 -0
- posturanet-0.1.0/src/posturanet/deliver/pr_body.py +218 -0
- posturanet-0.1.0/src/posturanet/errors.py +39 -0
- posturanet-0.1.0/src/posturanet/evidence/__init__.py +25 -0
- posturanet-0.1.0/src/posturanet/evidence/confidence.py +48 -0
- posturanet-0.1.0/src/posturanet/evidence/flowlogs.py +296 -0
- posturanet-0.1.0/src/posturanet/evidence/periodicity.py +41 -0
- posturanet-0.1.0/src/posturanet/gate.py +243 -0
- posturanet-0.1.0/src/posturanet/hooks.py +85 -0
- posturanet-0.1.0/src/posturanet/ingest/__init__.py +20 -0
- posturanet-0.1.0/src/posturanet/ingest/asff.py +132 -0
- posturanet-0.1.0/src/posturanet/ingest/controls.json +17 -0
- posturanet-0.1.0/src/posturanet/ingest/controls.py +17 -0
- posturanet-0.1.0/src/posturanet/ingest/manual.py +39 -0
- posturanet-0.1.0/src/posturanet/locate/__init__.py +145 -0
- posturanet-0.1.0/src/posturanet/locate/backend.py +67 -0
- posturanet-0.1.0/src/posturanet/locate/codeowners.py +54 -0
- posturanet-0.1.0/src/posturanet/locate/hcl_index.py +103 -0
- posturanet-0.1.0/src/posturanet/locate/tfstate.py +94 -0
- posturanet-0.1.0/src/posturanet/mcp/__init__.py +0 -0
- posturanet-0.1.0/src/posturanet/mcp/server.py +119 -0
- posturanet-0.1.0/src/posturanet/models/__init__.py +41 -0
- posturanet-0.1.0/src/posturanet/models/common.py +25 -0
- posturanet-0.1.0/src/posturanet/models/config.py +104 -0
- posturanet-0.1.0/src/posturanet/models/contract.py +59 -0
- posturanet-0.1.0/src/posturanet/models/finding.py +40 -0
- posturanet-0.1.0/src/posturanet/models/location.py +23 -0
- posturanet-0.1.0/src/posturanet/models/observations.py +56 -0
- posturanet-0.1.0/src/posturanet/models/run.py +55 -0
- posturanet-0.1.0/src/posturanet/models/validation.py +66 -0
- posturanet-0.1.0/src/posturanet/output/__init__.py +0 -0
- posturanet-0.1.0/src/posturanet/output/banner.py +98 -0
- posturanet-0.1.0/src/posturanet/output/console.py +77 -0
- posturanet-0.1.0/src/posturanet/output/symbols.py +29 -0
- posturanet-0.1.0/src/posturanet/pipeline.py +510 -0
- posturanet-0.1.0/src/posturanet/proposal.py +53 -0
- posturanet-0.1.0/src/posturanet/py.typed +0 -0
- posturanet-0.1.0/src/posturanet/synth/__init__.py +13 -0
- posturanet-0.1.0/src/posturanet/synth/hcl_render.py +160 -0
- posturanet-0.1.0/src/posturanet/synth/sg_rules.py +300 -0
- posturanet-0.1.0/src/posturanet/synth/sg_z3.py +157 -0
- posturanet-0.1.0/src/posturanet/validate/__init__.py +4 -0
- posturanet-0.1.0/src/posturanet/validate/guardrails.py +33 -0
- posturanet-0.1.0/src/posturanet/validate/invariants.py +112 -0
- posturanet-0.1.0/src/posturanet/validate/plan_scope.py +71 -0
- posturanet-0.1.0/src/posturanet/validate/recheck.py +71 -0
- posturanet-0.1.0/src/posturanet/validate/runner.py +273 -0
- posturanet-0.1.0/src/posturanet/validate/tf.py +92 -0
- posturanet-0.1.0/tests/conftest.py +33 -0
- posturanet-0.1.0/tests/fakes.py +170 -0
- posturanet-0.1.0/tests/fixtures/findings/sg_ssh_open.json +34 -0
- posturanet-0.1.0/tests/fixtures/flows/bastion_90d.json +6257 -0
- posturanet-0.1.0/tests/fixtures/tf/basic/.github/CODEOWNERS +3 -0
- posturanet-0.1.0/tests/fixtures/tf/basic/.posturanet.yml +6 -0
- posturanet-0.1.0/tests/fixtures/tf/basic/main.tf +9 -0
- posturanet-0.1.0/tests/fixtures/tf/basic/modules/bastion/main.tf +16 -0
- posturanet-0.1.0/tests/fixtures/tf/basic/state.fixture.json +56 -0
- posturanet-0.1.0/tests/integration/test_terraform_fmt.py +66 -0
- posturanet-0.1.0/tests/tests_support.py +19 -0
- posturanet-0.1.0/tests/unit/test_banner.py +68 -0
- posturanet-0.1.0/tests/unit/test_check.py +200 -0
- posturanet-0.1.0/tests/unit/test_cli.py +103 -0
- posturanet-0.1.0/tests/unit/test_config.py +32 -0
- posturanet-0.1.0/tests/unit/test_contracts.py +91 -0
- posturanet-0.1.0/tests/unit/test_deliver.py +120 -0
- posturanet-0.1.0/tests/unit/test_evidence.py +182 -0
- posturanet-0.1.0/tests/unit/test_gate.py +79 -0
- posturanet-0.1.0/tests/unit/test_ingest.py +83 -0
- posturanet-0.1.0/tests/unit/test_locate.py +161 -0
- posturanet-0.1.0/tests/unit/test_pipeline.py +143 -0
- posturanet-0.1.0/tests/unit/test_readonly.py +128 -0
- posturanet-0.1.0/tests/unit/test_synth.py +236 -0
- posturanet-0.1.0/tests/unit/test_validate.py +132 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.posturanet/runs/
|
|
10
|
+
.hypothesis/
|
|
11
|
+
# Never commit Terraform state or plans: they can contain secrets.
|
|
12
|
+
*.tfstate
|
|
13
|
+
*.tfstate.*
|
|
14
|
+
*.tfplan
|
|
15
|
+
tfplan
|
|
16
|
+
plan.json
|
|
17
|
+
.terraform/
|
|
18
|
+
.env
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to PosturaNet. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-11-03
|
|
8
|
+
First public release.
|
|
9
|
+
|
|
10
|
+
### Changed (license)
|
|
11
|
+
- PosturaNet is licensed under the Functional Source License 1.1 with an Apache 2.0 future license (`FSL-1.1-ALv2`, see `LICENSE.md`): free to use, read and change; no competing commercial products; each release becomes Apache 2.0 two years after it ships.
|
|
12
|
+
|
|
13
|
+
### Added (R1.5: PosturaNet check)
|
|
14
|
+
- `posturanet contract check --source [--file x.tf]`: offline check of the Terraform source against the contracts (no plan, no AWS). Literal inline ingress only; remote modules, `dynamic` blocks and non-literal values are UNKNOWN (US-7).
|
|
15
|
+
- `--github-check [--sha]`: publishes a `PosturaNet contract` check run (PASS → success, BLOCK → failure, UNKNOWN → neutral) with annotations on the broken contract line and the resource block (FR-29, US-4 for one repo). Needs `checks: write`.
|
|
16
|
+
- Results now carry each broken rule's contract line and the resource's file:line; text output shows `caused by:` per design system §7.
|
|
17
|
+
- GitHub Action `command: check`: runs `terraform plan -lock=false` itself (or takes `plan`), or `source-only: "true"` for an offline check; `fail-on-unknown`, `check-run`; output `check-url`. Example workflow in `integrations/github/`.
|
|
18
|
+
- `posturanet hook claude-code`: Claude Code PostToolUse hook. After a `.tf` edit it runs the source check and hands a BLOCK reason back to the agent; always exits 0. Wired into the Claude plugin (`skills/posturanet/plugin.json`) and `integrations/claude-code/`.
|
|
19
|
+
- MCP `check_change`: `plan_json` is now optional; without it, checks the source, optionally only `files`.
|
|
20
|
+
- Cursor rule and MCP config (`integrations/cursor/`), and `integrations/README.md` ("add PosturaNet to your pipeline").
|
|
21
|
+
- PRD: per-repo contract checks are in Community; the org-wide gate stays in Team (R4).
|
|
22
|
+
- Terminal logo: the PosturaNet logo (cloud, P, arrow) in half-block pixels with the brand gradient, shown by bare `posturanet` and `posturanet init` (on stderr). Only at an interactive terminal: never in JSON, pipes, CI, MCP or hooks. `NO_COLOR` drops color, non-UTF-8 terminals get a text-only version, `POSTURANET_NO_BANNER=1` turns it off.
|
|
23
|
+
|
|
24
|
+
### Added (R1: SG contracts)
|
|
25
|
+
- Read-only AWS layer (`aws/readonly.py`): every call goes through an allowlist; anything else raises `MutatingCallBlocked`. `posturanet init` prints the IAM policy generated from the same list (NFR-2, FR-47 partial).
|
|
26
|
+
- Evidence: VPC flow logs via CloudWatch Logs Insights, scoped to the security group's ENIs, chunked by 7 days, with a fail-closed 10,000-row cap. Offline `--evidence-file` JSON source. Confidence (high/medium/low/none), coverage and max gap, periodicity (daily/weekly/monthly/irregular/rare/one_off) with peak-hour detection (FR-11..FR-15).
|
|
27
|
+
- Behavior contracts: draft builder with default `must_deny` (world access on sensitive and exposed ports), YAML store per design system §5 (header, key order, flow-style rules, quoted account id). Approved contracts are used as-is and never overwritten; owner/ref edits survive regeneration (FR-14).
|
|
28
|
+
- Z3 synthesis of the minimal ingress rule set: replaces only open rules, keeps traffic the remaining rules already allow, supernets up to /16 internal and /24 public, quota-aware, lexicographic objective (exposure, then rule count). UNSAT and solver timeouts stop (FR-18).
|
|
29
|
+
- HCL rewrite of inline `ingress {}` blocks with literal values, fmt-clean output (checked against real `terraform fmt`), CRLF preserved. Dynamic blocks, non-literal values, block comments and separate rule resources stop (FR-20).
|
|
30
|
+
- Drift check from state ingress before synthesis, and from the plan's `before` during validation (FR-9).
|
|
31
|
+
- Validation runner in a temp copy: guardrails, `fmt -check`, `init` + `validate`, `plan -lock=false` + `show -json`, plan scope (target only, update in place), drift, plan-matches-fix, contract invariants (Z3 for allow, exact set checks for deny, with `model_scope`), finding re-check for EC2.13/14/18/19/53/54, optional tflint/checkov. PASS / FAIL / INCOMPLETE; anything but PASS opens no PR (FR-21..FR-26).
|
|
32
|
+
- PR delivery through the GitHub REST API: idempotency key + finding marker (one open PR per finding; same key → no writes, new key → update), base-branch content check, `posturanet/...` branches only, max open PRs, PR title/body/labels per design system §6, safety contract YAML (FR-27, FR-28, FR-30).
|
|
33
|
+
- S3 backend: `backend "s3"` block parsing and `s3:GetObject` through the read-only client; state kept in memory only (FR-5).
|
|
34
|
+
- CLI: `fix [--dry-run | --open-pr] [--evidence-file] [--window 90d]`, `contract build`, `contract check --plan` (exit 5 on BLOCK), `init`, `mcp`. Text output shows observed sources and the proposed diff.
|
|
35
|
+
- MCP server (stdio, MCP SDK 2.x) with read-only `explain_finding`, `propose_fix`, `check_change` (FR-40). Claude skill `skills/posturanet/SKILL.md` (FR-41). Composite GitHub Action `action/` (FR-39 partial).
|
|
36
|
+
- 129 tests, including hypothesis properties for synthesis and the Z3 allow check (zero wrongly-reported-safe), a no-mutating-AWS-calls test, a static "no boto3 outside `aws/`" test, and a real `terraform fmt` integration test.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- Flow-log query no longer filters on `flowDirection`, which exists only in custom log formats. On the default format it returned zero inbound flows, so every rule looked unused and could be proposed for removal. Inbound is now matched by `dstAddr` against the ENIs' own IPs, and an unparseable row stops the run instead of being dropped.
|
|
40
|
+
|
|
41
|
+
### Added (testing)
|
|
42
|
+
- `.github/workflows/posturanet.yml`: ruff, mypy, pytest (Python 3.11 and 3.12, real terraform for the fmt test), shellcheck on the Action entrypoint, and an offline smoke run.
|
|
43
|
+
- `sandbox/`: a throwaway Terraform repo for end-to-end tests: SG open on 22, an internal client, default-format VPC flow logs with 120-day retention, a read-only role, traffic scripts and a day-by-day test plan.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
- `fix` without `--dry-run` now runs the full pipeline (`check` mode); `--open-pr` delivers. `--dry-run` never calls AWS, GitHub or `terraform plan`, and writes nothing.
|
|
47
|
+
- A dry run without `--evidence-file` stops after locate, with next steps instead of "arrives in R1".
|
|
48
|
+
- `StageStop` can mark a stop as unknown (`?`, e.g. no evidence) and carry its own exit code (4 for validation FAIL).
|
|
49
|
+
|
|
50
|
+
### Not in R1 (tracked)
|
|
51
|
+
- Athena flow-log mode and `evidence.max_scan_gb`; IPv6 synthesis; `aws_security_group_rule` / `aws_vpc_security_group_ingress_rule` resources; `verify`, check runs (FR-29), freeze windows; MCP `open_fix_pr`; `init` writing config.
|
|
52
|
+
|
|
53
|
+
### Added (R0: dry-run skeleton)
|
|
54
|
+
- Package skeleton (`src/posturanet`), Pydantic v2 models for findings, locations, config and run records.
|
|
55
|
+
- ASFF ingest: Security Hub JSON → OCSF-aligned `Finding`, plus manual `sg-…` resource ids (FR-1, FR-3, FR-4).
|
|
56
|
+
- Locator v0: local Terraform state v4 → address (modules, index keys) → `file:line` via a minimal HCL scanner → owners from CODEOWNERS (FR-5..FR-8). Remote modules, S3 backend, duplicates and missing resources stop the run.
|
|
57
|
+
- `posturanet fix <target> --dry-run [--repo] [--state] [--format text|json] [--ascii]`, output per the design system. Exit codes 0 / 2 / 3.
|
|
58
|
+
- Synthetic fixtures (`tests/fixtures/`) and 36 tests.
|
|
59
|
+
|
|
60
|
+
### Notes
|
|
61
|
+
- Drift is reported as unknown until `terraform plan` lands in R1.
|
|
62
|
+
- The state fixture is named `state.fixture.json` so the repo's `*.tfstate` ignore rule stays in force.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Mihir Ajmera
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: posturanet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Evidence-backed remediation agent for AWS + Terraform. Fixes cloud security issues without breaking prod.
|
|
5
|
+
Project-URL: Homepage, https://www.posturanet.com
|
|
6
|
+
Project-URL: Source, https://github.com/Mihirajmera/posturanet
|
|
7
|
+
Project-URL: Issues, https://github.com/Mihirajmera/posturanet/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Mihirajmera/posturanet/blob/main/CHANGELOG.md
|
|
9
|
+
Author: PosturaNet
|
|
10
|
+
License-Expression: FSL-1.1-ALv2
|
|
11
|
+
License-File: LICENSE.md
|
|
12
|
+
Keywords: aws,cloud-security,mcp,remediation,security-groups,terraform,z3
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: boto3>=1.34
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
28
|
+
Requires-Dist: rich>=13.7
|
|
29
|
+
Requires-Dist: ruamel-yaml>=0.18
|
|
30
|
+
Requires-Dist: typer>=0.12
|
|
31
|
+
Requires-Dist: z3-solver>=4.12
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: boto3-stubs[ec2,logs,s3]>=1.34; extra == 'dev'
|
|
34
|
+
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
35
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
38
|
+
Provides-Extra: mcp
|
|
39
|
+
Requires-Dist: mcp<3,>=2.0; extra == 'mcp'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# PosturaNet
|
|
43
|
+
|
|
44
|
+
**Fixes cloud security issues without breaking prod.**
|
|
45
|
+
|
|
46
|
+
PosturaNet is a source-available remediation agent for AWS + Terraform. It takes a security finding, locates the owning Terraform and team, learns what actually uses the resource from flow logs and CloudTrail, records that as a *behavior contract*, and opens a pull request with the smallest fix that keeps every observed behavior working. It runs in your CI with a read-only role and never changes production.
|
|
47
|
+
|
|
48
|
+
It also checks every security group change, written by a person or an AI agent, against those contracts before it merges: a `PosturaNet contract` check on PRs, a step in any CI, and a hook for Claude Code and Cursor. See [integrations/](integrations/README.md).
|
|
49
|
+
|
|
50
|
+
> Status: early access (0.1.0). Security group ingress only.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
```bash
|
|
54
|
+
pip install posturanet # add [mcp] for the MCP server: pip install "posturanet[mcp]"
|
|
55
|
+
posturanet init # prints the read-only IAM policy PosturaNet needs
|
|
56
|
+
```
|
|
57
|
+
Python 3.11+. The GitHub Action is `Mihirajmera/posturanet/action@v0.1.0`; see [integrations/](integrations/README.md).
|
|
58
|
+
|
|
59
|
+
## Try it (offline, no AWS)
|
|
60
|
+
From a clone of this repo:
|
|
61
|
+
```bash
|
|
62
|
+
pip install -e ".[dev]"
|
|
63
|
+
posturanet fix tests/fixtures/findings/sg_ssh_open.json --dry-run \
|
|
64
|
+
--repo tests/fixtures/tf/basic --evidence-file tests/fixtures/flows/bastion_90d.json
|
|
65
|
+
```
|
|
66
|
+
This reads a synthetic finding, finds the Terraform block and its owner, builds a behavior contract from 89 days of synthetic flow logs, computes the smallest rule set with Z3, and prints the diff. Nothing is written.
|
|
67
|
+
|
|
68
|
+
With a read-only role: drop `--dry-run` to read flow logs and run `terraform plan`, then add `--open-pr`. Before changing security group ingress, run `posturanet contract check --plan plan.json`, or `--source` for an offline check of the edited HCL (exit 5 means BLOCK).
|
|
69
|
+
|
|
70
|
+
## Project docs
|
|
71
|
+
| Doc | Purpose |
|
|
72
|
+
|---|---|
|
|
73
|
+
| [docs/01_PRD.md](docs/01_PRD.md) | Product requirements: what we build, with FR/NFR IDs |
|
|
74
|
+
| [docs/02_ARCHITECTURE_AND_DATA.md](docs/02_ARCHITECTURE_AND_DATA.md) | Architecture, data models, schemas, queries |
|
|
75
|
+
| [AGENTS.md](AGENTS.md) | Rules for AI coding agents, and for PosturaNet's own runtime agent |
|
|
76
|
+
| [docs/04_DESIGN_SYSTEM.md](docs/04_DESIGN_SYSTEM.md) | CLI, PR, check-run and brand design system |
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
[Functional Source License 1.1, Apache 2.0 future license](LICENSE.md) (`FSL-1.1-ALv2`).
|
|
80
|
+
|
|
81
|
+
- **You can** use PosturaNet for free in your own company, read the code, change it and run it in your CI, including for clients if you provide services.
|
|
82
|
+
- **You can't** offer it, or a product built from it, as a competing commercial product or service.
|
|
83
|
+
- **After two years** each release becomes Apache 2.0, with no restrictions.
|
|
84
|
+
|
|
85
|
+
"PosturaNet" is a trademark: forks must use another name. Paid features ship separately in `posturanet-pro`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# PosturaNet
|
|
2
|
+
|
|
3
|
+
**Fixes cloud security issues without breaking prod.**
|
|
4
|
+
|
|
5
|
+
PosturaNet is a source-available remediation agent for AWS + Terraform. It takes a security finding, locates the owning Terraform and team, learns what actually uses the resource from flow logs and CloudTrail, records that as a *behavior contract*, and opens a pull request with the smallest fix that keeps every observed behavior working. It runs in your CI with a read-only role and never changes production.
|
|
6
|
+
|
|
7
|
+
It also checks every security group change, written by a person or an AI agent, against those contracts before it merges: a `PosturaNet contract` check on PRs, a step in any CI, and a hook for Claude Code and Cursor. See [integrations/](integrations/README.md).
|
|
8
|
+
|
|
9
|
+
> Status: early access (0.1.0). Security group ingress only.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
```bash
|
|
13
|
+
pip install posturanet # add [mcp] for the MCP server: pip install "posturanet[mcp]"
|
|
14
|
+
posturanet init # prints the read-only IAM policy PosturaNet needs
|
|
15
|
+
```
|
|
16
|
+
Python 3.11+. The GitHub Action is `Mihirajmera/posturanet/action@v0.1.0`; see [integrations/](integrations/README.md).
|
|
17
|
+
|
|
18
|
+
## Try it (offline, no AWS)
|
|
19
|
+
From a clone of this repo:
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e ".[dev]"
|
|
22
|
+
posturanet fix tests/fixtures/findings/sg_ssh_open.json --dry-run \
|
|
23
|
+
--repo tests/fixtures/tf/basic --evidence-file tests/fixtures/flows/bastion_90d.json
|
|
24
|
+
```
|
|
25
|
+
This reads a synthetic finding, finds the Terraform block and its owner, builds a behavior contract from 89 days of synthetic flow logs, computes the smallest rule set with Z3, and prints the diff. Nothing is written.
|
|
26
|
+
|
|
27
|
+
With a read-only role: drop `--dry-run` to read flow logs and run `terraform plan`, then add `--open-pr`. Before changing security group ingress, run `posturanet contract check --plan plan.json`, or `--source` for an offline check of the edited HCL (exit 5 means BLOCK).
|
|
28
|
+
|
|
29
|
+
## Project docs
|
|
30
|
+
| Doc | Purpose |
|
|
31
|
+
|---|---|
|
|
32
|
+
| [docs/01_PRD.md](docs/01_PRD.md) | Product requirements: what we build, with FR/NFR IDs |
|
|
33
|
+
| [docs/02_ARCHITECTURE_AND_DATA.md](docs/02_ARCHITECTURE_AND_DATA.md) | Architecture, data models, schemas, queries |
|
|
34
|
+
| [AGENTS.md](AGENTS.md) | Rules for AI coding agents, and for PosturaNet's own runtime agent |
|
|
35
|
+
| [docs/04_DESIGN_SYSTEM.md](docs/04_DESIGN_SYSTEM.md) | CLI, PR, check-run and brand design system |
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
[Functional Source License 1.1, Apache 2.0 future license](LICENSE.md) (`FSL-1.1-ALv2`).
|
|
39
|
+
|
|
40
|
+
- **You can** use PosturaNet for free in your own company, read the code, change it and run it in your CI, including for clients if you provide services.
|
|
41
|
+
- **You can't** offer it, or a product built from it, as a competing commercial product or service.
|
|
42
|
+
- **After two years** each release becomes Apache 2.0, with no restrictions.
|
|
43
|
+
|
|
44
|
+
"PosturaNet" is a trademark: forks must use another name. Paid features ship separately in `posturanet-pro`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Add PosturaNet to your pipeline
|
|
2
|
+
|
|
3
|
+
PosturaNet's contract check runs where you already work. It answers one question about a
|
|
4
|
+
security group change, whoever wrote it, a person or an AI agent: **does this still allow every
|
|
5
|
+
source that really uses the group, and does it keep denied networks closed?**
|
|
6
|
+
|
|
7
|
+
It needs a behavior contract for the group (`posturanet contract build <sg-id>`, then merge the
|
|
8
|
+
PR that adds `posturanet/contracts/<address>.yaml`). Groups without a contract are not checked.
|
|
9
|
+
|
|
10
|
+
| Result | Meaning | Exit code | Check run |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| `PASS` | every contract holds | 0 | `success` |
|
|
13
|
+
| `BLOCK` | a `must_allow` source would be cut off, a `must_deny` network opened, or the group deleted | 5 | `failure` |
|
|
14
|
+
| `UNKNOWN` | PosturaNet can't judge it (computed values, `dynamic` blocks, remote modules) | 3 | `neutral` |
|
|
15
|
+
|
|
16
|
+
UNKNOWN is never shown as green. Z3 checks cover ingress CIDR, port and protocol only; security
|
|
17
|
+
group references, prefix lists and egress are outside the model.
|
|
18
|
+
|
|
19
|
+
## Two ways to check
|
|
20
|
+
|
|
21
|
+
| | `--plan` | `--source` |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Input | `terraform show -json` output | the `.tf` files in the checkout |
|
|
24
|
+
| Needs | terraform, read-only AWS credentials | nothing (offline, under a second) |
|
|
25
|
+
| Judges | everything the plan knows | literal inline `ingress` blocks; anything else is UNKNOWN |
|
|
26
|
+
| Use for | the PR check | editor and AI agent hooks, quick local checks |
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
terraform plan -lock=false -out tfplan && terraform show -json tfplan > plan.json
|
|
30
|
+
posturanet contract check --plan plan.json
|
|
31
|
+
posturanet contract check --source --file modules/bastion/main.tf
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 1. GitHub pull requests
|
|
35
|
+
Copy [`github/posturanet-check.yml`](github/posturanet-check.yml) to `.github/workflows/`.
|
|
36
|
+
Every PR touching `*.tf` or a contract gets a **PosturaNet contract** check run. On BLOCK the
|
|
37
|
+
check fails and annotates the exact contract line and the resource block:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
✗ module.bastion.aws_security_group.this
|
|
41
|
+
breaks must_allow: 203.0.113.7/32 → 22/tcp (partner-sftp)
|
|
42
|
+
caused by: planned ingress in modules/bastion/main.tf:12
|
|
43
|
+
→ keep the rule, or update the contract in the same PR with owner approval
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The job needs `checks: write`. Make it a required status check in branch protection to stop a
|
|
47
|
+
merge on BLOCK. Set `fail-on-unknown: "false"` if UNKNOWN should not fail the job.
|
|
48
|
+
|
|
49
|
+
## 2. Any other CI
|
|
50
|
+
After your existing `terraform plan` step:
|
|
51
|
+
```bash
|
|
52
|
+
terraform show -json tfplan > plan.json
|
|
53
|
+
posturanet contract check --plan plan.json # exit 5 fails the build on BLOCK
|
|
54
|
+
```
|
|
55
|
+
To publish a GitHub check run from another CI, add `--github-check --sha <commit>` and set
|
|
56
|
+
`GITHUB_TOKEN` (with `checks: write`) and `GITHUB_REPOSITORY`.
|
|
57
|
+
|
|
58
|
+
## 3. Claude Code
|
|
59
|
+
Install the plugin in [`../skills/posturanet`](../skills/posturanet) (skill, MCP server and hook),
|
|
60
|
+
or copy [`claude-code/settings.json`](claude-code/settings.json) into `.claude/settings.json` and
|
|
61
|
+
[`claude-code/.mcp.json`](claude-code/.mcp.json) into the repo root.
|
|
62
|
+
|
|
63
|
+
After every edit to a `.tf` file, the hook runs `posturanet hook claude-code`. On BLOCK, Claude
|
|
64
|
+
gets the reason and fixes its own change before going on. On UNKNOWN it is told to check with a
|
|
65
|
+
plan. The hook only reads the file path from the event and never writes anything.
|
|
66
|
+
|
|
67
|
+
## 4. Cursor and other MCP agents
|
|
68
|
+
Copy [`cursor/mcp.json`](cursor/mcp.json) to `.cursor/mcp.json` and
|
|
69
|
+
[`cursor/posturanet.mdc`](cursor/posturanet.mdc) to `.cursor/rules/`. The rule tells the agent to
|
|
70
|
+
call `check_change` with the edited files and stop on BLOCK. Any MCP client works the same way:
|
|
71
|
+
run `posturanet mcp` and call `check_change`.
|
|
72
|
+
|
|
73
|
+
## What PosturaNet never does here
|
|
74
|
+
No AWS writes (the role is read-only), no `terraform apply`, no merge, no state lock
|
|
75
|
+
(`-lock=false`). Nothing leaves your runner except the check run sent to your own GitHub.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "posturanet"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Evidence-backed remediation agent for AWS + Terraform. Fixes cloud security issues without breaking prod."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "FSL-1.1-ALv2"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "PosturaNet" }]
|
|
14
|
+
keywords = ["aws", "terraform", "security-groups", "remediation", "cloud-security", "mcp", "z3"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
"Topic :: System :: Systems Administration",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pydantic>=2.6,<3",
|
|
30
|
+
"typer>=0.12",
|
|
31
|
+
"rich>=13.7",
|
|
32
|
+
"ruamel.yaml>=0.18",
|
|
33
|
+
"z3-solver>=4.12",
|
|
34
|
+
"boto3>=1.34",
|
|
35
|
+
"httpx>=0.27",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://www.posturanet.com"
|
|
40
|
+
Source = "https://github.com/Mihirajmera/posturanet"
|
|
41
|
+
Issues = "https://github.com/Mihirajmera/posturanet/issues"
|
|
42
|
+
Changelog = "https://github.com/Mihirajmera/posturanet/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
mcp = ["mcp>=2.0,<3"]
|
|
46
|
+
dev = ["pytest>=8", "ruff>=0.5", "mypy>=1.10", "hypothesis>=6.100", "boto3-stubs[ec2,logs,s3]>=1.34"]
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
posturanet = "posturanet.cli.app:main"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/posturanet"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = ["src/posturanet", "tests", "README.md", "CHANGELOG.md", "LICENSE.md", "pyproject.toml"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-q"
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py311"
|
|
64
|
+
src = ["src", "tests"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "I", "B", "UP", "SIM"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint.per-file-ignores]
|
|
70
|
+
# CLI output snapshots must match real line lengths.
|
|
71
|
+
"tests/**" = ["E501"] # long synthetic fixture data
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.11"
|
|
75
|
+
strict = true
|
|
76
|
+
mypy_path = "src"
|
|
77
|
+
plugins = ["pydantic.mypy"]
|
|
78
|
+
|
|
79
|
+
[[tool.mypy.overrides]]
|
|
80
|
+
module = ["z3", "z3.*"]
|
|
81
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# PosturaNet sandbox
|
|
2
|
+
|
|
3
|
+
A small, deliberately open AWS setup for testing PosturaNet end to end: one security group with SSH open to `0.0.0.0/0`, real traffic from known sources, and VPC flow logs. This folder is meant to become its own throwaway GitHub repo, which is the repo PosturaNet opens PRs against.
|
|
4
|
+
|
|
5
|
+
**Use a separate sandbox AWS account.** This opens port 22 to the internet on purpose. There's no key pair, so nobody can log in, but internet scanners will connect. That noise is part of the test.
|
|
6
|
+
|
|
7
|
+
## What it creates
|
|
8
|
+
| Resource | Why |
|
|
9
|
+
|---|---|
|
|
10
|
+
| VPC `10.42.0.0/16`, public subnet, internet gateway | Isolated network |
|
|
11
|
+
| `module.bastion`: SG with `22/tcp` from `0.0.0.0/0`, t3.micro with no key pair | The finding to fix |
|
|
12
|
+
| `aws_instance.client` (t3.nano, optional) | A steady internal source: connects to the bastion every 30 min |
|
|
13
|
+
| VPC flow logs (default format) → CloudWatch Logs, 120-day retention | The evidence |
|
|
14
|
+
| IAM role `posturanet-sandbox-readonly` | What PosturaNet and `terraform plan` run as |
|
|
15
|
+
|
|
16
|
+
**Cost:** roughly $5–15 a month, mostly the two instances (often $0 on the free tier). Flow-log ingestion and Logs Insights queries for this traffic volume cost cents. `terraform destroy` removes everything.
|
|
17
|
+
|
|
18
|
+
## Setup (day 0)
|
|
19
|
+
1. Copy this folder into a new private GitHub repo, e.g. `posturanet-sandbox`. In `.github/CODEOWNERS` and `.posturanet.yml`, put your GitHub handle.
|
|
20
|
+
2. Apply with **your own admin credentials** (PosturaNet never applies):
|
|
21
|
+
```bash
|
|
22
|
+
terraform init
|
|
23
|
+
terraform apply -var trusted_principal_arn=arn:aws:iam::<ACCOUNT_ID>:user/<you>
|
|
24
|
+
```
|
|
25
|
+
Note the outputs: `security_group_id`, `bastion_public_ip`, `posturanet_role_arn`.
|
|
26
|
+
3. Add a profile that assumes the read-only role, in `~/.aws/config`:
|
|
27
|
+
```ini
|
|
28
|
+
[profile posturanet-sandbox]
|
|
29
|
+
role_arn = <posturanet_role_arn output>
|
|
30
|
+
source_profile = default
|
|
31
|
+
region = us-east-1
|
|
32
|
+
```
|
|
33
|
+
Run every PosturaNet command with `AWS_PROFILE=posturanet-sandbox`, so PosturaNet's calls **and** `terraform plan` both use the read-only role. (`aws.role_arn` in `.posturanet.yml` only applies to PosturaNet's own calls, not to the plan.)
|
|
34
|
+
4. Start traffic from your laptop (an external daily source):
|
|
35
|
+
- Windows: `schtasks /Create /SC HOURLY /TN PosturaNetTraffic /TR "powershell -NoProfile -File C:\path\to\traffic\traffic.ps1 -Ip <bastion_public_ip>"`
|
|
36
|
+
- macOS/Linux: `0 9-18 * * * /path/to/traffic/traffic.sh <bastion_public_ip>` in `crontab -e`
|
|
37
|
+
5. Optional: copy `traffic/github-actions-traffic.yml.example` to `.github/workflows/traffic.yml` for irregular public sources.
|
|
38
|
+
6. Commit and push, including `.terraform.lock.hcl`. Never commit `terraform.tfstate`; it's git-ignored.
|
|
39
|
+
|
|
40
|
+
## Test plan
|
|
41
|
+
| When | Run | Expect |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Day 0 | `posturanet fix <sg-id> --dry-run` | Stops after locate (no evidence in a dry run). Owner from CODEOWNERS. |
|
|
44
|
+
| Day 1+ | `posturanet fix <sg-id>` | `evidence` stops: confidence low (under 14 days). Check the sources it lists match your laptop IP and `10.42.1.x`. |
|
|
45
|
+
| Day 1+ | `posturanet contract build <sg-id>` | Draft contract in `posturanet/contracts/`. Read it. |
|
|
46
|
+
| Day 14+ | `posturanet fix <sg-id>` | Real `terraform plan`, all required checks PASS, `VALIDATED`, risk medium. |
|
|
47
|
+
| Day 14+ | `GITHUB_TOKEN=… posturanet fix <sg-id> --open-pr` | A PR in the sandbox repo. Re-run: "PR already open", no duplicate. |
|
|
48
|
+
| After merge | `terraform apply`, keep traffic running | Your laptop and the client still connect; the scanners stop. |
|
|
49
|
+
| Any time | Edit ingress to drop your IP, `terraform plan -out p && terraform show -json p > plan.json`, `posturanet contract check --plan plan.json` | Exit 5, BLOCK, names your IP. |
|
|
50
|
+
| Day 60+ | `posturanet fix <sg-id>` | Confidence high, risk low (if no needs-review sources). |
|
|
51
|
+
|
|
52
|
+
Use `--format json` and keep the output of every run; those records are the test log.
|
|
53
|
+
|
|
54
|
+
## What to watch for
|
|
55
|
+
- **Scanners.** Port 22 open to the internet collects many one-off public sources. R1 keeps `needs_review` sources in the fix (the safe direction) and labels the PR. Watch how many appear, and whether the rule quota stops the fix. This is the first real-world question the sandbox answers.
|
|
56
|
+
- **Plan permissions.** If `terraform plan` fails with AccessDenied under the read-only role, note the action; that's the input for a narrower customer policy.
|
|
57
|
+
- **Anything that stops.** STOPPED is a valid answer, but record the reason: each one is either correct fail-closed behavior or a bug.
|
|
58
|
+
|
|
59
|
+
## Clean up
|
|
60
|
+
```bash
|
|
61
|
+
terraform destroy -var trusted_principal_arn=<same as apply>
|
|
62
|
+
```
|
|
63
|
+
and remove the scheduled task or cron entry.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Read-only AWS access (NFR-2, architecture doc 7.3).
|
|
2
|
+
|
|
3
|
+
Every AWS call in PosturaNet goes through `ReadOnlyClient`. Operations not on the allowlist
|
|
4
|
+
raise `MutatingCallBlocked` before anything reaches the network. The generated IAM policy
|
|
5
|
+
(`iam_policy()`) is built from the same list so the two can't drift apart.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import fnmatch
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from posturanet.errors import PosturaNetError
|
|
15
|
+
|
|
16
|
+
ALLOWLIST: tuple[str, ...] = (
|
|
17
|
+
"ec2:Describe*",
|
|
18
|
+
"logs:StartQuery",
|
|
19
|
+
"logs:GetQueryResults",
|
|
20
|
+
"logs:DescribeLogGroups",
|
|
21
|
+
"athena:StartQueryExecution",
|
|
22
|
+
"athena:GetQueryExecution",
|
|
23
|
+
"athena:GetQueryResults",
|
|
24
|
+
"s3:GetObject",
|
|
25
|
+
"s3:ListBucket",
|
|
26
|
+
"iam:Get*",
|
|
27
|
+
"iam:List*",
|
|
28
|
+
"iam:GenerateServiceLastAccessedDetails",
|
|
29
|
+
"iam:SimulatePrincipalPolicy",
|
|
30
|
+
"securityhub:GetFindings",
|
|
31
|
+
"access-analyzer:Get*",
|
|
32
|
+
"access-analyzer:List*",
|
|
33
|
+
"sts:GetCallerIdentity",
|
|
34
|
+
"sts:AssumeRole",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# boto3 service names that differ from IAM action prefixes.
|
|
38
|
+
_IAM_PREFIX = {"accessanalyzer": "access-analyzer"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MutatingCallBlocked(PosturaNetError):
|
|
42
|
+
"""An AWS operation outside the read-only allowlist was attempted."""
|
|
43
|
+
|
|
44
|
+
exit_code = 10
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def iam_action(service: str, method: str) -> str:
|
|
48
|
+
"""`("ec2", "describe_network_interfaces")` -> `"ec2:DescribeNetworkInterfaces"`."""
|
|
49
|
+
prefix = _IAM_PREFIX.get(service, service)
|
|
50
|
+
op = "".join(part.capitalize() for part in method.split("_"))
|
|
51
|
+
return f"{prefix}:{op}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_allowed(action: str) -> bool:
|
|
55
|
+
return any(fnmatch.fnmatchcase(action, pattern) for pattern in ALLOWLIST)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_PAGINATOR = re.compile(r"^(get_paginator|can_paginate|get_waiter)$")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ReadOnlyClient:
|
|
62
|
+
"""Wraps a boto3 client; only allowlisted operations are reachable."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, client: Any, service: str) -> None:
|
|
65
|
+
self._client = client
|
|
66
|
+
self._service = service
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def service(self) -> str:
|
|
70
|
+
return self._service
|
|
71
|
+
|
|
72
|
+
def __getattr__(self, name: str) -> Any:
|
|
73
|
+
if name.startswith("_") or _PAGINATOR.match(name):
|
|
74
|
+
# Paginators and waiters would bypass the per-call check.
|
|
75
|
+
raise MutatingCallBlocked(f"{self._service}.{name} is not available in PosturaNet")
|
|
76
|
+
action = iam_action(self._service, name)
|
|
77
|
+
if not is_allowed(action):
|
|
78
|
+
raise MutatingCallBlocked(
|
|
79
|
+
f"{action} is not on the read-only allowlist; PosturaNet never writes to AWS"
|
|
80
|
+
)
|
|
81
|
+
return getattr(self._client, name)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def iam_policy() -> dict[str, Any]:
|
|
85
|
+
"""IAM policy document a customer attaches to PosturaNet's role."""
|
|
86
|
+
return {
|
|
87
|
+
"Version": "2012-10-17",
|
|
88
|
+
"Statement": [
|
|
89
|
+
{
|
|
90
|
+
"Sid": "PosturaNetReadOnly",
|
|
91
|
+
"Effect": "Allow",
|
|
92
|
+
"Action": sorted(ALLOWLIST),
|
|
93
|
+
"Resource": "*",
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
}
|