sovereign-observer 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.
- sovereign_observer-0.1.0/.gitignore +67 -0
- sovereign_observer-0.1.0/Makefile +17 -0
- sovereign_observer-0.1.0/PKG-INFO +215 -0
- sovereign_observer-0.1.0/README.md +194 -0
- sovereign_observer-0.1.0/pyproject.toml +47 -0
- sovereign_observer-0.1.0/scripts/vendor_engine.py +114 -0
- sovereign_observer-0.1.0/sovereign_mcp/__init__.py +3 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/__init__.py +1 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/hcl_fixer.py +524 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/policy_engine.py +284 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/remediation_registry.py +972 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/remediation_service.py +452 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/remediation_supplement.py +4946 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/__init__.py +0 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/checkov_scanner.py +829 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/compliance_crosswalk.py +120 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/hcl_locator.py +185 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/hcl_to_plan.py +292 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/regulatory_overlays.py +262 -0
- sovereign_observer-0.1.0/sovereign_mcp/_vendor/scanners/terraform_inventory.py +1434 -0
- sovereign_observer-0.1.0/sovereign_mcp/engine.py +145 -0
- sovereign_observer-0.1.0/sovereign_mcp/org.py +161 -0
- sovereign_observer-0.1.0/sovereign_mcp/server.py +243 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/__init__.py +0 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/compliance.py +190 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/explain.py +115 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/fix.py +133 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/org_policy.py +281 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/scan.py +166 -0
- sovereign_observer-0.1.0/sovereign_mcp/tools/templates.py +393 -0
- sovereign_observer-0.1.0/tests/__init__.py +0 -0
- sovereign_observer-0.1.0/tests/conftest.py +7 -0
- sovereign_observer-0.1.0/tests/test_compliance.py +99 -0
- sovereign_observer-0.1.0/tests/test_fix.py +124 -0
- sovereign_observer-0.1.0/tests/test_org_policy.py +308 -0
- sovereign_observer-0.1.0/tests/test_parity.py +126 -0
- sovereign_observer-0.1.0/tests/test_scan_golden.py +90 -0
- sovereign_observer-0.1.0/tests/test_stdio_protocol.py +87 -0
- sovereign_observer-0.1.0/tests/test_templates.py +60 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# ── Secrets — NEVER commit these ───────────────────────────────────────────
|
|
2
|
+
# Losing/leaking ENCRYPTION_KEY (Fernet) makes all stored customer cloud
|
|
3
|
+
# credentials undecryptable; a leaked GCP service account or Firebase key is a
|
|
4
|
+
# full account compromise. Keep this list strict.
|
|
5
|
+
.env
|
|
6
|
+
.env.*
|
|
7
|
+
!.env.example
|
|
8
|
+
*.key
|
|
9
|
+
secret.key
|
|
10
|
+
*service_account*.json
|
|
11
|
+
gcp_service_account.json
|
|
12
|
+
firebase*.json
|
|
13
|
+
serviceAccount*.json
|
|
14
|
+
*.pem
|
|
15
|
+
credentials.json
|
|
16
|
+
|
|
17
|
+
# ── Databases / local state ────────────────────────────────────────────────
|
|
18
|
+
*.db
|
|
19
|
+
*.sqlite
|
|
20
|
+
*.sqlite3
|
|
21
|
+
instance/
|
|
22
|
+
.runtime/
|
|
23
|
+
|
|
24
|
+
# ── Terraform plan artifacts (may embed resource values) ──────────────────
|
|
25
|
+
*.tfplan
|
|
26
|
+
*.tfstate
|
|
27
|
+
*.tfstate.*
|
|
28
|
+
.terraform/
|
|
29
|
+
|
|
30
|
+
# ── Python ─────────────────────────────────────────────────────────────────
|
|
31
|
+
__pycache__/
|
|
32
|
+
*.pyc
|
|
33
|
+
.venv/
|
|
34
|
+
venv/
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
|
|
37
|
+
# ── Node / frontend ──────────────────────────────────────────────────────────
|
|
38
|
+
node_modules/
|
|
39
|
+
frontend/build/
|
|
40
|
+
frontend/build-check*/
|
|
41
|
+
frontend/public/screenshots/
|
|
42
|
+
|
|
43
|
+
# ── Misc ───────────────────────────────────────────────────────────────────
|
|
44
|
+
.DS_Store
|
|
45
|
+
|
|
46
|
+
.vercel
|
|
47
|
+
|
|
48
|
+
# ── Outreach data (scraped contact emails + send history — never public) ───
|
|
49
|
+
marketing/outreach/lead_messages_generated.csv
|
|
50
|
+
marketing/outreach/send_queue.csv
|
|
51
|
+
# The timestamped backups refresh_queue.py writes hold the same scraped
|
|
52
|
+
# addresses as send_queue.csv itself, so they need the same rule — an ignored
|
|
53
|
+
# file whose backups are committed is not ignored.
|
|
54
|
+
marketing/outreach/send_queue.*.csv
|
|
55
|
+
marketing/outreach/sent_log.csv
|
|
56
|
+
# Agent output: proposed actions with lead names, addresses and draft bodies.
|
|
57
|
+
marketing/outreach/approval_queue.json
|
|
58
|
+
marketing/outreach/followup_log.csv
|
|
59
|
+
|
|
60
|
+
# ── MCP server: generated engine copy ──────────────────────────────────────
|
|
61
|
+
# scripts/vendor_engine.py copies the backend's IaC engine modules here at
|
|
62
|
+
# build time. Committing them would put a second copy of the rule logic in git,
|
|
63
|
+
# which is exactly the drift the vendoring design exists to prevent.
|
|
64
|
+
integrations/mcp-server/sovereign_mcp/_vendor/
|
|
65
|
+
integrations/mcp-server/dist/
|
|
66
|
+
integrations/mcp-server/build/
|
|
67
|
+
integrations/mcp-server/*.egg-info/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
PYTHON ?= python
|
|
2
|
+
|
|
3
|
+
.PHONY: vendor test wheel clean
|
|
4
|
+
|
|
5
|
+
## Copy the IaC engine in from the backend. Required before building.
|
|
6
|
+
vendor:
|
|
7
|
+
$(PYTHON) scripts/vendor_engine.py
|
|
8
|
+
|
|
9
|
+
test: vendor
|
|
10
|
+
$(PYTHON) -m pytest tests/ -q
|
|
11
|
+
|
|
12
|
+
## Vendor THEN build — a wheel built without vendoring ships no engine.
|
|
13
|
+
wheel: vendor
|
|
14
|
+
$(PYTHON) -m build --wheel
|
|
15
|
+
|
|
16
|
+
clean:
|
|
17
|
+
rm -rf dist build *.egg-info sovereign_mcp/_vendor
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sovereign-observer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scan AI-generated Terraform for security misconfigurations, in your editor, before it is committed.
|
|
5
|
+
Project-URL: Homepage, https://sovereignobserver.com
|
|
6
|
+
Author: Sovereign Observer
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Keywords: checkov,compliance,cspm,iac,mcp,security,terraform
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: checkov==3.3.11
|
|
17
|
+
Requires-Dist: mcp>=1.2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Sovereign MCP
|
|
23
|
+
|
|
24
|
+
Your AI assistant writes Terraform. This checks it before you do.
|
|
25
|
+
|
|
26
|
+
An MCP server that scans Terraform for security misconfigurations **while the code is
|
|
27
|
+
being generated**, not after it lands in a pull request. It runs locally, needs no
|
|
28
|
+
account, and your infrastructure code never leaves your machine.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
You: "add an RDS instance for the orders service"
|
|
32
|
+
Assistant: [writes HCL] → [scans it] → [fixes 4 findings] → shows you the result
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Why this exists
|
|
38
|
+
|
|
39
|
+
Provider defaults optimise for *it works*, not *it is safe*. Terraform generated from a
|
|
40
|
+
model's memory is routinely unencrypted, publicly reachable, or missing deletion
|
|
41
|
+
protection — and the cost of fixing that rises steeply the further it travels. In the
|
|
42
|
+
editor it is one attribute. In a PR it is a review cycle. In production it is an incident.
|
|
43
|
+
|
|
44
|
+
CI already catches this. CI catches it three days and one argument later.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
### Claude Code
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude mcp add sovereign -- uvx sovereign-observer
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Cursor
|
|
57
|
+
|
|
58
|
+
`~/.cursor/mcp.json`:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"sovereign": {
|
|
64
|
+
"command": "uvx",
|
|
65
|
+
"args": ["sovereign-observer"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### VS Code (GitHub Copilot)
|
|
72
|
+
|
|
73
|
+
`.vscode/mcp.json`:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"servers": {
|
|
78
|
+
"sovereign": {
|
|
79
|
+
"type": "stdio",
|
|
80
|
+
"command": "uvx",
|
|
81
|
+
"args": ["sovereign-observer"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Windsurf
|
|
88
|
+
|
|
89
|
+
`~/.codeium/windsurf/mcp_config.json`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"sovereign": {
|
|
95
|
+
"command": "uvx",
|
|
96
|
+
"args": ["sovereign-observer"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
First run downloads the engine (~100 MB) and takes a moment. After that it is local and
|
|
103
|
+
fast.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Tools
|
|
108
|
+
|
|
109
|
+
| Tool | What it does |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `scan_terraform` | Scan HCL — from disk or an unsaved buffer. Returns findings by severity with file and line. |
|
|
112
|
+
| `explain_finding` | The full remediation for one finding: what is wrong and the exact Terraform to fix it. |
|
|
113
|
+
| `apply_fixes` | Apply the mechanically-safe fixes and return patched HCL. |
|
|
114
|
+
| `secure_template` | A hardened starting point for a resource type, so the insecure version never gets written. |
|
|
115
|
+
| `check_compliance` | Map findings to SOC 2, ISO 27001, NIST 800-53, PCI-DSS, DORA, NIS2, NCA (Saudi), NESA (UAE). |
|
|
116
|
+
| `framework_coverage` | Which articles of a regulation automated scanning can and cannot evidence. |
|
|
117
|
+
| `org_requirements` | Your organization's own rules for a resource type — *before* the code is written. |
|
|
118
|
+
| `org_status` | Whether org policy is in force, or built-in rules only. |
|
|
119
|
+
|
|
120
|
+
You do not call these. The assistant does, on its own, because the server tells it to.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Organization policy
|
|
125
|
+
|
|
126
|
+
Everything above works with no account. Connecting an organization adds **your
|
|
127
|
+
company's own rules** to the same local evaluation:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
export SOVEREIGN_TOKEN=... # Integrations → GitHub in the dashboard
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The difference this makes is in *when* the rule applies. Without it, the assistant writes
|
|
134
|
+
Terraform and then finds out it was wrong. With it:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
You: "add an RDS instance for the orders service"
|
|
138
|
+
Assistant: → org_requirements("aws_db_instance")
|
|
139
|
+
← "backup_retention_period must be at least 365"
|
|
140
|
+
"region must be one of: eu-west-1, eu-central-1"
|
|
141
|
+
[writes Terraform that already satisfies both]
|
|
142
|
+
→ scan_terraform → clean
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The rule is supplied to the generator, not applied to the output. That is the whole point
|
|
146
|
+
— a violation that never gets written costs nothing to fix.
|
|
147
|
+
|
|
148
|
+
Company rules appear in scans tagged `source: org_policy`, so a developer can always tell
|
|
149
|
+
a company requirement from a built-in one. They are authored in the dashboard as YAML and
|
|
150
|
+
enforced identically in the editor, in CI, and in a cloud scan.
|
|
151
|
+
|
|
152
|
+
**This does not change what leaves your machine.** Rules come down; code never goes up.
|
|
153
|
+
The only request this server makes is a `GET` for your org's rules —
|
|
154
|
+
`tests/test_org_policy.py::test_no_terraform_is_ever_uploaded` asserts that at the
|
|
155
|
+
transport, and asserts an unconnected install opens no socket at all. If the API is
|
|
156
|
+
unreachable or the token is rejected, the built-in rules still run locally and the scan
|
|
157
|
+
still works.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## What it does not do
|
|
162
|
+
|
|
163
|
+
Worth stating plainly, because a security tool that overstates its scope is worse than no
|
|
164
|
+
tool:
|
|
165
|
+
|
|
166
|
+
- **It is not a compliance assessment.** `check_compliance` returns control *mappings* —
|
|
167
|
+
evidence that shortens an audit. Every framework it maps also carries governance,
|
|
168
|
+
process and training obligations no configuration scanner can observe. A clean scan is
|
|
169
|
+
not a compliant organisation.
|
|
170
|
+
- **NCA control identifiers are provisional**, pending reconciliation against the
|
|
171
|
+
authority's published catalogue. Cite the subdomain names.
|
|
172
|
+
- **`apply_fixes` is deliberately narrow.** It applies only single-attribute, in-place
|
|
173
|
+
changes from a hand-verified allowlist, and never overwrites a value wired to a variable
|
|
174
|
+
or expression. Everything else stays advisory, because a mechanical fix that is
|
|
175
|
+
syntactically clean can still take a running system down.
|
|
176
|
+
- **It scans Terraform**, not live cloud accounts, container images, or dependencies.
|
|
177
|
+
|
|
178
|
+
For live multi-cloud posture management, attack-path analysis and audit-ready reporting,
|
|
179
|
+
this is the editor-side slice of [Sovereign Observer](https://sovereignobserver.com).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Privacy
|
|
184
|
+
|
|
185
|
+
The scan runs in this process, on your machine. There is no API key, no account, and no
|
|
186
|
+
network call in the default path — the server works with networking disabled. Your
|
|
187
|
+
Terraform is never uploaded.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
From a checkout of the product repository:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The IaC engine is **copied in from the backend at build time** by
|
|
200
|
+
`scripts/vendor_engine.py`, into a gitignored `sovereign_mcp/_vendor/`. That is
|
|
201
|
+
deliberate: the repository holds exactly one copy of the rule logic, so the editor and CI
|
|
202
|
+
can never disagree about whether a resource is insecure. `sovereign_mcp/engine.py` vendors
|
|
203
|
+
on demand in a source checkout, so there is no build step for day-to-day work.
|
|
204
|
+
|
|
205
|
+
Never edit anything under `_vendor/`. Edit the backend module and re-run the script.
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python -m pytest tests/ -q
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`tests/test_parity.py` is the one that matters — it fails if the vendored copy drifts from
|
|
212
|
+
its source, or if the Checkov pin stops matching the backend's.
|
|
213
|
+
|
|
214
|
+
Licensed Apache-2.0. Built on [Checkov](https://github.com/bridgecrewio/checkov)
|
|
215
|
+
(Apache-2.0).
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Sovereign MCP
|
|
2
|
+
|
|
3
|
+
Your AI assistant writes Terraform. This checks it before you do.
|
|
4
|
+
|
|
5
|
+
An MCP server that scans Terraform for security misconfigurations **while the code is
|
|
6
|
+
being generated**, not after it lands in a pull request. It runs locally, needs no
|
|
7
|
+
account, and your infrastructure code never leaves your machine.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
You: "add an RDS instance for the orders service"
|
|
11
|
+
Assistant: [writes HCL] → [scans it] → [fixes 4 findings] → shows you the result
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why this exists
|
|
17
|
+
|
|
18
|
+
Provider defaults optimise for *it works*, not *it is safe*. Terraform generated from a
|
|
19
|
+
model's memory is routinely unencrypted, publicly reachable, or missing deletion
|
|
20
|
+
protection — and the cost of fixing that rises steeply the further it travels. In the
|
|
21
|
+
editor it is one attribute. In a PR it is a review cycle. In production it is an incident.
|
|
22
|
+
|
|
23
|
+
CI already catches this. CI catches it three days and one argument later.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
claude mcp add sovereign -- uvx sovereign-observer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Cursor
|
|
36
|
+
|
|
37
|
+
`~/.cursor/mcp.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"sovereign": {
|
|
43
|
+
"command": "uvx",
|
|
44
|
+
"args": ["sovereign-observer"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### VS Code (GitHub Copilot)
|
|
51
|
+
|
|
52
|
+
`.vscode/mcp.json`:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"servers": {
|
|
57
|
+
"sovereign": {
|
|
58
|
+
"type": "stdio",
|
|
59
|
+
"command": "uvx",
|
|
60
|
+
"args": ["sovereign-observer"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Windsurf
|
|
67
|
+
|
|
68
|
+
`~/.codeium/windsurf/mcp_config.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"sovereign": {
|
|
74
|
+
"command": "uvx",
|
|
75
|
+
"args": ["sovereign-observer"]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
First run downloads the engine (~100 MB) and takes a moment. After that it is local and
|
|
82
|
+
fast.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Tools
|
|
87
|
+
|
|
88
|
+
| Tool | What it does |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `scan_terraform` | Scan HCL — from disk or an unsaved buffer. Returns findings by severity with file and line. |
|
|
91
|
+
| `explain_finding` | The full remediation for one finding: what is wrong and the exact Terraform to fix it. |
|
|
92
|
+
| `apply_fixes` | Apply the mechanically-safe fixes and return patched HCL. |
|
|
93
|
+
| `secure_template` | A hardened starting point for a resource type, so the insecure version never gets written. |
|
|
94
|
+
| `check_compliance` | Map findings to SOC 2, ISO 27001, NIST 800-53, PCI-DSS, DORA, NIS2, NCA (Saudi), NESA (UAE). |
|
|
95
|
+
| `framework_coverage` | Which articles of a regulation automated scanning can and cannot evidence. |
|
|
96
|
+
| `org_requirements` | Your organization's own rules for a resource type — *before* the code is written. |
|
|
97
|
+
| `org_status` | Whether org policy is in force, or built-in rules only. |
|
|
98
|
+
|
|
99
|
+
You do not call these. The assistant does, on its own, because the server tells it to.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Organization policy
|
|
104
|
+
|
|
105
|
+
Everything above works with no account. Connecting an organization adds **your
|
|
106
|
+
company's own rules** to the same local evaluation:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
export SOVEREIGN_TOKEN=... # Integrations → GitHub in the dashboard
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The difference this makes is in *when* the rule applies. Without it, the assistant writes
|
|
113
|
+
Terraform and then finds out it was wrong. With it:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
You: "add an RDS instance for the orders service"
|
|
117
|
+
Assistant: → org_requirements("aws_db_instance")
|
|
118
|
+
← "backup_retention_period must be at least 365"
|
|
119
|
+
"region must be one of: eu-west-1, eu-central-1"
|
|
120
|
+
[writes Terraform that already satisfies both]
|
|
121
|
+
→ scan_terraform → clean
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The rule is supplied to the generator, not applied to the output. That is the whole point
|
|
125
|
+
— a violation that never gets written costs nothing to fix.
|
|
126
|
+
|
|
127
|
+
Company rules appear in scans tagged `source: org_policy`, so a developer can always tell
|
|
128
|
+
a company requirement from a built-in one. They are authored in the dashboard as YAML and
|
|
129
|
+
enforced identically in the editor, in CI, and in a cloud scan.
|
|
130
|
+
|
|
131
|
+
**This does not change what leaves your machine.** Rules come down; code never goes up.
|
|
132
|
+
The only request this server makes is a `GET` for your org's rules —
|
|
133
|
+
`tests/test_org_policy.py::test_no_terraform_is_ever_uploaded` asserts that at the
|
|
134
|
+
transport, and asserts an unconnected install opens no socket at all. If the API is
|
|
135
|
+
unreachable or the token is rejected, the built-in rules still run locally and the scan
|
|
136
|
+
still works.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## What it does not do
|
|
141
|
+
|
|
142
|
+
Worth stating plainly, because a security tool that overstates its scope is worse than no
|
|
143
|
+
tool:
|
|
144
|
+
|
|
145
|
+
- **It is not a compliance assessment.** `check_compliance` returns control *mappings* —
|
|
146
|
+
evidence that shortens an audit. Every framework it maps also carries governance,
|
|
147
|
+
process and training obligations no configuration scanner can observe. A clean scan is
|
|
148
|
+
not a compliant organisation.
|
|
149
|
+
- **NCA control identifiers are provisional**, pending reconciliation against the
|
|
150
|
+
authority's published catalogue. Cite the subdomain names.
|
|
151
|
+
- **`apply_fixes` is deliberately narrow.** It applies only single-attribute, in-place
|
|
152
|
+
changes from a hand-verified allowlist, and never overwrites a value wired to a variable
|
|
153
|
+
or expression. Everything else stays advisory, because a mechanical fix that is
|
|
154
|
+
syntactically clean can still take a running system down.
|
|
155
|
+
- **It scans Terraform**, not live cloud accounts, container images, or dependencies.
|
|
156
|
+
|
|
157
|
+
For live multi-cloud posture management, attack-path analysis and audit-ready reporting,
|
|
158
|
+
this is the editor-side slice of [Sovereign Observer](https://sovereignobserver.com).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Privacy
|
|
163
|
+
|
|
164
|
+
The scan runs in this process, on your machine. There is no API key, no account, and no
|
|
165
|
+
network call in the default path — the server works with networking disabled. Your
|
|
166
|
+
Terraform is never uploaded.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
From a checkout of the product repository:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install -e ".[dev]"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The IaC engine is **copied in from the backend at build time** by
|
|
179
|
+
`scripts/vendor_engine.py`, into a gitignored `sovereign_mcp/_vendor/`. That is
|
|
180
|
+
deliberate: the repository holds exactly one copy of the rule logic, so the editor and CI
|
|
181
|
+
can never disagree about whether a resource is insecure. `sovereign_mcp/engine.py` vendors
|
|
182
|
+
on demand in a source checkout, so there is no build step for day-to-day work.
|
|
183
|
+
|
|
184
|
+
Never edit anything under `_vendor/`. Edit the backend module and re-run the script.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python -m pytest tests/ -q
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`tests/test_parity.py` is the one that matters — it fails if the vendored copy drifts from
|
|
191
|
+
its source, or if the Checkov pin stops matching the backend's.
|
|
192
|
+
|
|
193
|
+
Licensed Apache-2.0. Built on [Checkov](https://github.com/bridgecrewio/checkov)
|
|
194
|
+
(Apache-2.0).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sovereign-observer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scan AI-generated Terraform for security misconfigurations, in your editor, before it is committed."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Sovereign Observer" }]
|
|
13
|
+
keywords = ["mcp", "terraform", "iac", "security", "checkov", "cspm", "compliance"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Security",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=1.2.0",
|
|
25
|
+
# Pinned to the same version the backend runs, so an MCP scan and a CI scan
|
|
26
|
+
# evaluate identical policy. See tests/test_parity.py.
|
|
27
|
+
"checkov==3.3.11",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=7.4"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
sovereign-observer = "sovereign_mcp.server:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://sovereignobserver.com"
|
|
38
|
+
|
|
39
|
+
# `packages` picks up sovereign_mcp/_vendor as well — hatchling does not read
|
|
40
|
+
# .gitignore, so the generated engine copy ships in the wheel even though it is
|
|
41
|
+
# never committed. Run scripts/vendor_engine.py BEFORE building, or the wheel
|
|
42
|
+
# will be missing its engine. `make wheel` does both in order.
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["sovereign_mcp"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Copy the backend's IaC engine modules into ``sovereign_mcp/_vendor``.
|
|
3
|
+
|
|
4
|
+
Why this exists
|
|
5
|
+
---------------
|
|
6
|
+
The MCP server must run on a developer's laptop with no Flask app and no
|
|
7
|
+
network, but it must evaluate *exactly* the policy the backend evaluates. Two
|
|
8
|
+
hand-maintained copies of the rule logic would be worse than shipping no MCP at
|
|
9
|
+
all — a finding that appears in the editor but not in CI (or the reverse)
|
|
10
|
+
destroys trust in both.
|
|
11
|
+
|
|
12
|
+
So the backend stays the single source of truth in git, and this script
|
|
13
|
+
mechanically copies the modules at build time. ``sovereign_mcp/_vendor`` is
|
|
14
|
+
**generated and gitignored** — never edit it, and never commit it.
|
|
15
|
+
|
|
16
|
+
The copied modules are pure stdlib (verified: no Flask, no SQLAlchemy, no
|
|
17
|
+
``current_app``). The directory layout below mirrors the backend's
|
|
18
|
+
``services/`` + ``services/scanners/`` shape on purpose, so the relative import
|
|
19
|
+
``from ..remediation_service import RemediationService`` inside
|
|
20
|
+
``checkov_scanner`` keeps resolving without patching a single line.
|
|
21
|
+
|
|
22
|
+
Run directly (``python scripts/vendor_engine.py``) or let ``sovereign_mcp.engine``
|
|
23
|
+
call it automatically on first import in a source checkout.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
# relpath under the backend -> relpath under _vendor/
|
|
33
|
+
MODULES = {
|
|
34
|
+
"backend/app/services/remediation_service.py": "remediation_service.py",
|
|
35
|
+
"backend/app/services/scanners/checkov_scanner.py": "scanners/checkov_scanner.py",
|
|
36
|
+
"backend/app/services/scanners/hcl_locator.py": "scanners/hcl_locator.py",
|
|
37
|
+
"backend/app/services/scanners/hcl_to_plan.py": "scanners/hcl_to_plan.py",
|
|
38
|
+
"backend/app/services/scanners/regulatory_overlays.py": "scanners/regulatory_overlays.py",
|
|
39
|
+
"backend/app/services/scanners/compliance_crosswalk.py": "scanners/compliance_crosswalk.py",
|
|
40
|
+
# The HCL fix-applier the GitHub Action uses. Shared so a fix applied in the
|
|
41
|
+
# editor is byte-for-byte the fix the Action would have applied in CI.
|
|
42
|
+
"integrations/github-action/scan.py": "hcl_fixer.py",
|
|
43
|
+
# Org custom-policy evaluation. These four run the same YAML rules against
|
|
44
|
+
# the same inventory shape the server does, so an org rule means the same
|
|
45
|
+
# thing in the editor as it does in a scan. They sit at the _vendor root
|
|
46
|
+
# because policy_engine imports remediation_registry by absolute name.
|
|
47
|
+
"backend/app/services/scanners/terraform_inventory.py": "scanners/terraform_inventory.py",
|
|
48
|
+
"policy_engine.py": "policy_engine.py",
|
|
49
|
+
"remediation_registry.py": "remediation_registry.py",
|
|
50
|
+
"remediation_supplement.py": "remediation_supplement.py",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_GENERATED_HEADER = (
|
|
54
|
+
"# ⚠ GENERATED FILE — DO NOT EDIT.\n"
|
|
55
|
+
"# Copied verbatim from {source} by scripts/vendor_engine.py.\n"
|
|
56
|
+
"# Edit the backend module instead; this copy is regenerated at build time.\n"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def find_app_root(start: Optional[Path] = None) -> Optional[Path]:
|
|
61
|
+
"""Walk up from ``start`` looking for the 'main application' directory.
|
|
62
|
+
|
|
63
|
+
Returns None when running from an installed wheel, where there is no
|
|
64
|
+
backend source tree to copy from (and none is needed — the wheel already
|
|
65
|
+
carries the vendored modules).
|
|
66
|
+
"""
|
|
67
|
+
here = (start or Path(__file__).resolve()).resolve()
|
|
68
|
+
for parent in [here, *here.parents]:
|
|
69
|
+
candidate = parent / "backend" / "app" / "services" / "scanners" / "checkov_scanner.py"
|
|
70
|
+
if candidate.is_file():
|
|
71
|
+
return parent
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def vendor(app_root: Optional[Path] = None, dest: Optional[Path] = None) -> Path:
|
|
76
|
+
"""Copy the engine modules into ``dest``. Returns the vendor directory."""
|
|
77
|
+
app_root = app_root or find_app_root()
|
|
78
|
+
if app_root is None:
|
|
79
|
+
raise FileNotFoundError(
|
|
80
|
+
"Could not locate the backend source tree. Run this from a checkout "
|
|
81
|
+
"of the Sovereign Observer repository."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
dest = dest or (Path(__file__).resolve().parent.parent / "sovereign_mcp" / "_vendor")
|
|
85
|
+
if dest.exists():
|
|
86
|
+
shutil.rmtree(dest)
|
|
87
|
+
(dest / "scanners").mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
# Package markers so relative imports inside the copied modules resolve.
|
|
90
|
+
(dest / "__init__.py").write_text(
|
|
91
|
+
'"""Generated vendor tree — see scripts/vendor_engine.py."""\n', encoding="utf-8"
|
|
92
|
+
)
|
|
93
|
+
(dest / "scanners" / "__init__.py").write_text("", encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
for src_rel, dst_rel in MODULES.items():
|
|
96
|
+
src = app_root / src_rel
|
|
97
|
+
if not src.is_file():
|
|
98
|
+
raise FileNotFoundError(f"Expected engine module missing: {src}")
|
|
99
|
+
body = src.read_text(encoding="utf-8")
|
|
100
|
+
(dest / dst_rel).write_text(
|
|
101
|
+
_GENERATED_HEADER.format(source=src_rel) + body, encoding="utf-8"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return dest
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main() -> int:
|
|
108
|
+
dest = vendor()
|
|
109
|
+
print(f"vendored {len(MODULES)} engine modules -> {dest}")
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generated vendor tree — see scripts/vendor_engine.py."""
|