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.
Files changed (95) hide show
  1. posturanet-0.1.0/.gitignore +18 -0
  2. posturanet-0.1.0/CHANGELOG.md +62 -0
  3. posturanet-0.1.0/LICENSE.md +105 -0
  4. posturanet-0.1.0/PKG-INFO +85 -0
  5. posturanet-0.1.0/README.md +44 -0
  6. posturanet-0.1.0/integrations/README.md +75 -0
  7. posturanet-0.1.0/pyproject.toml +81 -0
  8. posturanet-0.1.0/sandbox/README.md +63 -0
  9. posturanet-0.1.0/src/posturanet/__init__.py +3 -0
  10. posturanet-0.1.0/src/posturanet/__main__.py +3 -0
  11. posturanet-0.1.0/src/posturanet/aws/__init__.py +3 -0
  12. posturanet-0.1.0/src/posturanet/aws/readonly.py +96 -0
  13. posturanet-0.1.0/src/posturanet/aws/session.py +55 -0
  14. posturanet-0.1.0/src/posturanet/cli/__init__.py +0 -0
  15. posturanet-0.1.0/src/posturanet/cli/app.py +283 -0
  16. posturanet-0.1.0/src/posturanet/contracts/__init__.py +18 -0
  17. posturanet-0.1.0/src/posturanet/contracts/builder.py +63 -0
  18. posturanet-0.1.0/src/posturanet/contracts/invariants.py +7 -0
  19. posturanet-0.1.0/src/posturanet/contracts/store.py +135 -0
  20. posturanet-0.1.0/src/posturanet/deliver/__init__.py +4 -0
  21. posturanet-0.1.0/src/posturanet/deliver/checks.py +98 -0
  22. posturanet-0.1.0/src/posturanet/deliver/github.py +231 -0
  23. posturanet-0.1.0/src/posturanet/deliver/idempotency.py +49 -0
  24. posturanet-0.1.0/src/posturanet/deliver/pr_body.py +218 -0
  25. posturanet-0.1.0/src/posturanet/errors.py +39 -0
  26. posturanet-0.1.0/src/posturanet/evidence/__init__.py +25 -0
  27. posturanet-0.1.0/src/posturanet/evidence/confidence.py +48 -0
  28. posturanet-0.1.0/src/posturanet/evidence/flowlogs.py +296 -0
  29. posturanet-0.1.0/src/posturanet/evidence/periodicity.py +41 -0
  30. posturanet-0.1.0/src/posturanet/gate.py +243 -0
  31. posturanet-0.1.0/src/posturanet/hooks.py +85 -0
  32. posturanet-0.1.0/src/posturanet/ingest/__init__.py +20 -0
  33. posturanet-0.1.0/src/posturanet/ingest/asff.py +132 -0
  34. posturanet-0.1.0/src/posturanet/ingest/controls.json +17 -0
  35. posturanet-0.1.0/src/posturanet/ingest/controls.py +17 -0
  36. posturanet-0.1.0/src/posturanet/ingest/manual.py +39 -0
  37. posturanet-0.1.0/src/posturanet/locate/__init__.py +145 -0
  38. posturanet-0.1.0/src/posturanet/locate/backend.py +67 -0
  39. posturanet-0.1.0/src/posturanet/locate/codeowners.py +54 -0
  40. posturanet-0.1.0/src/posturanet/locate/hcl_index.py +103 -0
  41. posturanet-0.1.0/src/posturanet/locate/tfstate.py +94 -0
  42. posturanet-0.1.0/src/posturanet/mcp/__init__.py +0 -0
  43. posturanet-0.1.0/src/posturanet/mcp/server.py +119 -0
  44. posturanet-0.1.0/src/posturanet/models/__init__.py +41 -0
  45. posturanet-0.1.0/src/posturanet/models/common.py +25 -0
  46. posturanet-0.1.0/src/posturanet/models/config.py +104 -0
  47. posturanet-0.1.0/src/posturanet/models/contract.py +59 -0
  48. posturanet-0.1.0/src/posturanet/models/finding.py +40 -0
  49. posturanet-0.1.0/src/posturanet/models/location.py +23 -0
  50. posturanet-0.1.0/src/posturanet/models/observations.py +56 -0
  51. posturanet-0.1.0/src/posturanet/models/run.py +55 -0
  52. posturanet-0.1.0/src/posturanet/models/validation.py +66 -0
  53. posturanet-0.1.0/src/posturanet/output/__init__.py +0 -0
  54. posturanet-0.1.0/src/posturanet/output/banner.py +98 -0
  55. posturanet-0.1.0/src/posturanet/output/console.py +77 -0
  56. posturanet-0.1.0/src/posturanet/output/symbols.py +29 -0
  57. posturanet-0.1.0/src/posturanet/pipeline.py +510 -0
  58. posturanet-0.1.0/src/posturanet/proposal.py +53 -0
  59. posturanet-0.1.0/src/posturanet/py.typed +0 -0
  60. posturanet-0.1.0/src/posturanet/synth/__init__.py +13 -0
  61. posturanet-0.1.0/src/posturanet/synth/hcl_render.py +160 -0
  62. posturanet-0.1.0/src/posturanet/synth/sg_rules.py +300 -0
  63. posturanet-0.1.0/src/posturanet/synth/sg_z3.py +157 -0
  64. posturanet-0.1.0/src/posturanet/validate/__init__.py +4 -0
  65. posturanet-0.1.0/src/posturanet/validate/guardrails.py +33 -0
  66. posturanet-0.1.0/src/posturanet/validate/invariants.py +112 -0
  67. posturanet-0.1.0/src/posturanet/validate/plan_scope.py +71 -0
  68. posturanet-0.1.0/src/posturanet/validate/recheck.py +71 -0
  69. posturanet-0.1.0/src/posturanet/validate/runner.py +273 -0
  70. posturanet-0.1.0/src/posturanet/validate/tf.py +92 -0
  71. posturanet-0.1.0/tests/conftest.py +33 -0
  72. posturanet-0.1.0/tests/fakes.py +170 -0
  73. posturanet-0.1.0/tests/fixtures/findings/sg_ssh_open.json +34 -0
  74. posturanet-0.1.0/tests/fixtures/flows/bastion_90d.json +6257 -0
  75. posturanet-0.1.0/tests/fixtures/tf/basic/.github/CODEOWNERS +3 -0
  76. posturanet-0.1.0/tests/fixtures/tf/basic/.posturanet.yml +6 -0
  77. posturanet-0.1.0/tests/fixtures/tf/basic/main.tf +9 -0
  78. posturanet-0.1.0/tests/fixtures/tf/basic/modules/bastion/main.tf +16 -0
  79. posturanet-0.1.0/tests/fixtures/tf/basic/state.fixture.json +56 -0
  80. posturanet-0.1.0/tests/integration/test_terraform_fmt.py +66 -0
  81. posturanet-0.1.0/tests/tests_support.py +19 -0
  82. posturanet-0.1.0/tests/unit/test_banner.py +68 -0
  83. posturanet-0.1.0/tests/unit/test_check.py +200 -0
  84. posturanet-0.1.0/tests/unit/test_cli.py +103 -0
  85. posturanet-0.1.0/tests/unit/test_config.py +32 -0
  86. posturanet-0.1.0/tests/unit/test_contracts.py +91 -0
  87. posturanet-0.1.0/tests/unit/test_deliver.py +120 -0
  88. posturanet-0.1.0/tests/unit/test_evidence.py +182 -0
  89. posturanet-0.1.0/tests/unit/test_gate.py +79 -0
  90. posturanet-0.1.0/tests/unit/test_ingest.py +83 -0
  91. posturanet-0.1.0/tests/unit/test_locate.py +161 -0
  92. posturanet-0.1.0/tests/unit/test_pipeline.py +143 -0
  93. posturanet-0.1.0/tests/unit/test_readonly.py +128 -0
  94. posturanet-0.1.0/tests/unit/test_synth.py +236 -0
  95. 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,3 @@
1
+ """PosturaNet: evidence-backed remediation agent for AWS + Terraform."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from posturanet.cli.app import main
2
+
3
+ main()
@@ -0,0 +1,3 @@
1
+ from posturanet.aws.readonly import ALLOWLIST, MutatingCallBlocked, ReadOnlyClient, iam_policy
2
+
3
+ __all__ = ["ALLOWLIST", "MutatingCallBlocked", "ReadOnlyClient", "iam_policy"]
@@ -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
+ }