aws-iplens 1.0.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 (152) hide show
  1. aws_iplens-1.0.0/PKG-INFO +402 -0
  2. aws_iplens-1.0.0/README.md +367 -0
  3. aws_iplens-1.0.0/aws_iplens.egg-info/PKG-INFO +402 -0
  4. aws_iplens-1.0.0/aws_iplens.egg-info/SOURCES.txt +150 -0
  5. aws_iplens-1.0.0/aws_iplens.egg-info/dependency_links.txt +1 -0
  6. aws_iplens-1.0.0/aws_iplens.egg-info/entry_points.txt +2 -0
  7. aws_iplens-1.0.0/aws_iplens.egg-info/requires.txt +13 -0
  8. aws_iplens-1.0.0/aws_iplens.egg-info/top_level.txt +1 -0
  9. aws_iplens-1.0.0/iplens/__init__.py +8 -0
  10. aws_iplens-1.0.0/iplens/__main__.py +3 -0
  11. aws_iplens-1.0.0/iplens/accounts.py +548 -0
  12. aws_iplens-1.0.0/iplens/attribution.py +167 -0
  13. aws_iplens-1.0.0/iplens/aws.py +213 -0
  14. aws_iplens-1.0.0/iplens/cidrplan.py +588 -0
  15. aws_iplens-1.0.0/iplens/cli.py +34 -0
  16. aws_iplens-1.0.0/iplens/collector.py +868 -0
  17. aws_iplens-1.0.0/iplens/config.py +50 -0
  18. aws_iplens-1.0.0/iplens/crypto.py +43 -0
  19. aws_iplens-1.0.0/iplens/db.py +650 -0
  20. aws_iplens-1.0.0/iplens/declutter.py +147 -0
  21. aws_iplens-1.0.0/iplens/diagram.py +867 -0
  22. aws_iplens-1.0.0/iplens/diff.py +305 -0
  23. aws_iplens-1.0.0/iplens/environment.py +155 -0
  24. aws_iplens-1.0.0/iplens/export.py +60 -0
  25. aws_iplens-1.0.0/iplens/extended.py +1298 -0
  26. aws_iplens-1.0.0/iplens/extgraph.py +281 -0
  27. aws_iplens-1.0.0/iplens/flowlogs.py +309 -0
  28. aws_iplens-1.0.0/iplens/jobs.py +491 -0
  29. aws_iplens-1.0.0/iplens/logging_setup.py +104 -0
  30. aws_iplens-1.0.0/iplens/ownership.py +520 -0
  31. aws_iplens-1.0.0/iplens/queries.py +841 -0
  32. aws_iplens-1.0.0/iplens/report.py +394 -0
  33. aws_iplens-1.0.0/iplens/retention.py +113 -0
  34. aws_iplens-1.0.0/iplens/rules.py +485 -0
  35. aws_iplens-1.0.0/iplens/s3state.py +253 -0
  36. aws_iplens-1.0.0/iplens/scheduler.py +155 -0
  37. aws_iplens-1.0.0/iplens/scope.py +287 -0
  38. aws_iplens-1.0.0/iplens/settings.py +242 -0
  39. aws_iplens-1.0.0/iplens/static/copy.js +15 -0
  40. aws_iplens-1.0.0/iplens/static/declutter.js +126 -0
  41. aws_iplens-1.0.0/iplens/static/favicon.svg +15 -0
  42. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_AWS-Lambda_48.svg +10 -0
  43. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Amazon-EC2_48.svg +10 -0
  44. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Amazon-ElastiCache_48.svg +10 -0
  45. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Amazon-Elastic-Container-Service_48.svg +10 -0
  46. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Amazon-OpenSearch-Service_48.svg +10 -0
  47. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Amazon-RDS_48.svg +10 -0
  48. aws_iplens-1.0.0/iplens/static/icons/aws/Arch_Elastic-Load-Balancing_48.svg +10 -0
  49. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Amazon-VPC_Elastic-Network-Interface_48.svg +7 -0
  50. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Amazon-VPC_Endpoints_48.svg +7 -0
  51. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Amazon-VPC_NAT-Gateway_48.svg +7 -0
  52. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Elastic-Load-Balancing_Application-Load-Balancer_48.svg +7 -0
  53. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Elastic-Load-Balancing_Gateway-Load-Balancer_48.svg +7 -0
  54. aws_iplens-1.0.0/iplens/static/icons/aws/Res_Elastic-Load-Balancing_Network-Load-Balancer_48.svg +7 -0
  55. aws_iplens-1.0.0/iplens/static/icons/aws/Virtual-private-cloud-VPC_32.svg +8 -0
  56. aws_iplens-1.0.0/iplens/static/icons/ext/apigateway.svg +5 -0
  57. aws_iplens-1.0.0/iplens/static/icons/ext/dynamodb.svg +5 -0
  58. aws_iplens-1.0.0/iplens/static/icons/ext/events.svg +5 -0
  59. aws_iplens-1.0.0/iplens/static/icons/ext/internet.svg +5 -0
  60. aws_iplens-1.0.0/iplens/static/icons/ext/kafka.svg +5 -0
  61. aws_iplens-1.0.0/iplens/static/icons/ext/kinesis.svg +5 -0
  62. aws_iplens-1.0.0/iplens/static/icons/ext/other.svg +5 -0
  63. aws_iplens-1.0.0/iplens/static/icons/ext/pcx.svg +5 -0
  64. aws_iplens-1.0.0/iplens/static/icons/ext/resolver.svg +5 -0
  65. aws_iplens-1.0.0/iplens/static/icons/ext/route53.svg +5 -0
  66. aws_iplens-1.0.0/iplens/static/icons/ext/s3.svg +5 -0
  67. aws_iplens-1.0.0/iplens/static/icons/ext/secretsmanager.svg +5 -0
  68. aws_iplens-1.0.0/iplens/static/icons/ext/sns.svg +5 -0
  69. aws_iplens-1.0.0/iplens/static/icons/ext/sqs.svg +5 -0
  70. aws_iplens-1.0.0/iplens/static/icons/ext/states.svg +5 -0
  71. aws_iplens-1.0.0/iplens/static/icons/ext/tgw-attachment.svg +5 -0
  72. aws_iplens-1.0.0/iplens/static/icons/ext/tgw.svg +5 -0
  73. aws_iplens-1.0.0/iplens/static/jobs.js +186 -0
  74. aws_iplens-1.0.0/iplens/static/localtime.js +25 -0
  75. aws_iplens-1.0.0/iplens/static/style.css +260 -0
  76. aws_iplens-1.0.0/iplens/static/vendor/cytoscape.LICENSE +19 -0
  77. aws_iplens-1.0.0/iplens/static/vendor/cytoscape.min.js +31 -0
  78. aws_iplens-1.0.0/iplens/static/vendor/dagre.LICENSE +19 -0
  79. aws_iplens-1.0.0/iplens/static/vendor/dagre.min.js +801 -0
  80. aws_iplens-1.0.0/iplens/static/visual.js +2180 -0
  81. aws_iplens-1.0.0/iplens/suggestions.py +480 -0
  82. aws_iplens-1.0.0/iplens/templates/_macros.html +29 -0
  83. aws_iplens-1.0.0/iplens/templates/account_form.html +101 -0
  84. aws_iplens-1.0.0/iplens/templates/base.html +104 -0
  85. aws_iplens-1.0.0/iplens/templates/cidrplan.html +164 -0
  86. aws_iplens-1.0.0/iplens/templates/diff.html +102 -0
  87. aws_iplens-1.0.0/iplens/templates/eni.html +45 -0
  88. aws_iplens-1.0.0/iplens/templates/error.html +7 -0
  89. aws_iplens-1.0.0/iplens/templates/ips.html +99 -0
  90. aws_iplens-1.0.0/iplens/templates/logs.html +31 -0
  91. aws_iplens-1.0.0/iplens/templates/overview.html +125 -0
  92. aws_iplens-1.0.0/iplens/templates/ownership.html +241 -0
  93. aws_iplens-1.0.0/iplens/templates/report.html +264 -0
  94. aws_iplens-1.0.0/iplens/templates/rule_form.html +109 -0
  95. aws_iplens-1.0.0/iplens/templates/rules.html +61 -0
  96. aws_iplens-1.0.0/iplens/templates/scope.html +77 -0
  97. aws_iplens-1.0.0/iplens/templates/settings.html +255 -0
  98. aws_iplens-1.0.0/iplens/templates/subnet.html +56 -0
  99. aws_iplens-1.0.0/iplens/templates/suggestions.html +46 -0
  100. aws_iplens-1.0.0/iplens/templates/tfrepo.html +85 -0
  101. aws_iplens-1.0.0/iplens/templates/trends.html +74 -0
  102. aws_iplens-1.0.0/iplens/templates/visual.html +268 -0
  103. aws_iplens-1.0.0/iplens/terraform.py +439 -0
  104. aws_iplens-1.0.0/iplens/tfrepo.py +1827 -0
  105. aws_iplens-1.0.0/iplens/trends.py +355 -0
  106. aws_iplens-1.0.0/iplens/viewstate.py +232 -0
  107. aws_iplens-1.0.0/iplens/visual.py +471 -0
  108. aws_iplens-1.0.0/iplens/web.py +2050 -0
  109. aws_iplens-1.0.0/pyproject.toml +81 -0
  110. aws_iplens-1.0.0/setup.cfg +4 -0
  111. aws_iplens-1.0.0/tests/test_accounts.py +452 -0
  112. aws_iplens-1.0.0/tests/test_attribution.py +149 -0
  113. aws_iplens-1.0.0/tests/test_aws.py +148 -0
  114. aws_iplens-1.0.0/tests/test_cidrplan.py +335 -0
  115. aws_iplens-1.0.0/tests/test_cli.py +14 -0
  116. aws_iplens-1.0.0/tests/test_collector.py +767 -0
  117. aws_iplens-1.0.0/tests/test_context.py +398 -0
  118. aws_iplens-1.0.0/tests/test_contrast.py +99 -0
  119. aws_iplens-1.0.0/tests/test_crawl_caps.py +84 -0
  120. aws_iplens-1.0.0/tests/test_db.py +96 -0
  121. aws_iplens-1.0.0/tests/test_declutter.py +269 -0
  122. aws_iplens-1.0.0/tests/test_diagram.py +297 -0
  123. aws_iplens-1.0.0/tests/test_diff.py +184 -0
  124. aws_iplens-1.0.0/tests/test_environment.py +163 -0
  125. aws_iplens-1.0.0/tests/test_export.py +80 -0
  126. aws_iplens-1.0.0/tests/test_extended.py +934 -0
  127. aws_iplens-1.0.0/tests/test_identity.py +243 -0
  128. aws_iplens-1.0.0/tests/test_jobs.py +206 -0
  129. aws_iplens-1.0.0/tests/test_logging.py +56 -0
  130. aws_iplens-1.0.0/tests/test_msk.py +184 -0
  131. aws_iplens-1.0.0/tests/test_ownership.py +538 -0
  132. aws_iplens-1.0.0/tests/test_queries.py +273 -0
  133. aws_iplens-1.0.0/tests/test_report.py +202 -0
  134. aws_iplens-1.0.0/tests/test_retention.py +153 -0
  135. aws_iplens-1.0.0/tests/test_rules.py +187 -0
  136. aws_iplens-1.0.0/tests/test_rules_form.py +194 -0
  137. aws_iplens-1.0.0/tests/test_s3state.py +570 -0
  138. aws_iplens-1.0.0/tests/test_scheduler.py +190 -0
  139. aws_iplens-1.0.0/tests/test_scope.py +206 -0
  140. aws_iplens-1.0.0/tests/test_settings.py +60 -0
  141. aws_iplens-1.0.0/tests/test_shared_attribution.py +283 -0
  142. aws_iplens-1.0.0/tests/test_suggestions.py +223 -0
  143. aws_iplens-1.0.0/tests/test_terraform.py +486 -0
  144. aws_iplens-1.0.0/tests/test_tf_cancel.py +268 -0
  145. aws_iplens-1.0.0/tests/test_tfrepo.py +695 -0
  146. aws_iplens-1.0.0/tests/test_tfsync_accounts.py +298 -0
  147. aws_iplens-1.0.0/tests/test_trends.py +229 -0
  148. aws_iplens-1.0.0/tests/test_visual.py +363 -0
  149. aws_iplens-1.0.0/tests/test_visual_browser.py +223 -0
  150. aws_iplens-1.0.0/tests/test_visual_controls.py +279 -0
  151. aws_iplens-1.0.0/tests/test_web.py +610 -0
  152. aws_iplens-1.0.0/tests/test_web_accounts.py +905 -0
@@ -0,0 +1,402 @@
1
+ Metadata-Version: 2.4
2
+ Name: aws-iplens
3
+ Version: 1.0.0
4
+ Summary: Local web app for private IPv4 usage visibility & optimization across AWS accounts
5
+ Author: Amir Amiri
6
+ Project-URL: Homepage, https://github.com/Amiri83/IPLens
7
+ Project-URL: Source, https://github.com/Amiri83/IPLens
8
+ Project-URL: Issues, https://github.com/Amiri83/IPLens/issues
9
+ Keywords: aws,vpc,ipv4,subnet,eni,network,visualization
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Flask
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Networking
20
+ Classifier: Topic :: System :: Systems Administration
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: flask>=3.0
24
+ Requires-Dist: boto3>=1.34
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: cryptography>=42
27
+ Requires-Dist: openpyxl>=3.1
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8; extra == "dev"
30
+ Requires-Dist: moto[cloudformation,cloudtrail,dynamodb,ec2,ecs,elbv2,events,iam,lambda,logs,resourcegroupstaggingapi,s3,secretsmanager,sns,sqs,stepfunctions,sts]>=5.0; extra == "dev"
31
+ Requires-Dist: ruff>=0.5; extra == "dev"
32
+ Requires-Dist: playwright>=1.40; extra == "dev"
33
+ Requires-Dist: build>=1.2; extra == "dev"
34
+ Requires-Dist: twine>=6.1; extra == "dev"
35
+
36
+ # IPLens
37
+ Local web app for private IPv4 usage visibility & optimization across one or more AWS accounts
38
+
39
+ ## Features
40
+
41
+ - **Discovery** – read-only snapshots of VPCs, subnets, ENIs and their private IPs per account,
42
+ with a VPC → subnet tree and a per-subnet address grid.
43
+ - **IP List** – every private IP in one filterable table (VPC, subnet, resource type, status,
44
+ environment, owner), exportable to Excel.
45
+ - **Visual** – per-VPC diagram of subnets and resource ENIs with AWS icons, several layouts and
46
+ SVG / draw.io export.
47
+ - **Extended view** – optional crawl of regional services (SNS, SQS, DynamoDB, EventBridge, S3,
48
+ API Gateway, Step Functions, Lambda, ECS, MSK, ...) and network edges, each connection ranked by
49
+ evidence; opt-in flow log analysis.
50
+ - **Ownership** – who manages each resource: CloudFormation, IaC tags, optional Terraform state,
51
+ optional CloudTrail creator, or unmanaged; plus tag gaps.
52
+ - **Trends** – scheduled refresh, snapshot retention with daily downsampling, used-IP charts and
53
+ a "full in ~N days" forecast per subnet and VPC.
54
+ - **Diff** – compare two snapshots: ENIs / IPs added, removed and changed, net delta per subnet,
55
+ top consumers.
56
+ - **CIDR planner** – free blocks and fragmentation per VPC CIDR, a Fit tool for new subnets and
57
+ secondary-CIDR proposals checked for overlaps.
58
+ - **Report** – one self-contained, printable HTML report per account, optionally with identifiers
59
+ redacted.
60
+ - **Rules & Suggestions** – IP-saving suggestions, filtered by your own rules (GUI or YAML).
61
+ - **Terraform** (optional) – read-only sync of local Terraform repos for ownership, environments
62
+ and drift.
63
+ - **Multi-account, local and read-only** – one local app for many accounts; every AWS call passes
64
+ a read-only allowlist guard.
65
+
66
+ ## Install
67
+
68
+ Requires Python 3.11+. The PyPI distribution is `aws-iplens`; the command it installs is `iplens`.
69
+
70
+ ```bash
71
+ pipx install aws-iplens # or: python3 -m pip install aws-iplens
72
+ iplens --version
73
+ ```
74
+
75
+ From a checkout (development):
76
+
77
+ ```bash
78
+ python3 -m venv .venv
79
+ .venv/bin/pip install -e ".[dev]"
80
+ ```
81
+
82
+ ## Run
83
+
84
+ ```bash
85
+ iplens # http://127.0.0.1:8077
86
+ iplens --port 8080 # another port
87
+ iplens --home /path/to/data # another data directory (default: $IPLENS_HOME or ~/.iplens)
88
+ ```
89
+
90
+ Open the printed URL in a browser. AWS credentials are configured per account in the UI
91
+ (Settings → Accounts); see below.
92
+
93
+ ## Data location
94
+
95
+ Everything lives in one directory: `--home`, else `$IPLENS_HOME`, else `~/.iplens` (created with
96
+ mode `0700`). Delete it to reset IPLens completely.
97
+
98
+ | path | contents |
99
+ |---|---|
100
+ | `iplens.db` | SQLite: accounts, snapshots, crawled services, rules, settings, view preferences |
101
+ | `secret.key` | Fernet key that encrypts stored AWS secrets (mode `0600`) |
102
+ | `flask.secret` | session signing key |
103
+ | `logs/` | application log (the log directory can be changed in Settings) |
104
+ | `tf-cache/` | Terraform working data of repo syncs (only when Terraform sync is used) |
105
+
106
+ ## Security
107
+
108
+ - **Local only.** The server binds to `127.0.0.1` by default and rejects requests whose `Host`
109
+ header is not a local name (DNS-rebinding protection). Do not expose it with `--host 0.0.0.0`
110
+ on a shared network: it has no login.
111
+ - **Read-only AWS access.** Every AWS call goes through an allowlist guard (see
112
+ [Read-only by construction](#read-only-by-construction)); IPLens never changes your account.
113
+ - **Secrets.** Access keys and session tokens are Fernet-encrypted in SQLite (or kept in memory
114
+ only), never logged and never rendered back. Environment variable values, policy documents and
115
+ secret values seen while crawling are matched in memory and never stored.
116
+ - **Web.** Every form POST needs a per-session CSRF token; session cookies are `HttpOnly` and
117
+ `SameSite=Strict`. All front-end assets are vendored: the browser makes no external requests.
118
+ - **Releases** are published from GitHub Actions with PyPI Trusted Publishing (OIDC); no API
119
+ token is stored in the repository.
120
+
121
+ ## Using IPLens
122
+
123
+ 1. **Settings → Accounts** – add one entry per AWS account (display name, region, auth mode) and
124
+ use **Test**. Auth modes: environment/default chain, named profile (dropdown of
125
+ `~/.aws/config` + `~/.aws/credentials`, SSO profiles included), access key + secret, or
126
+ temporary credentials (paste three fields, an `export AWS_…` block, or `aws sts` JSON including
127
+ `Expiration`; the form shows "expires in Xm"). Secrets and session tokens are Fernet-encrypted
128
+ in SQLite, or kept in process memory only with **memory only**; they are never logged or
129
+ rendered. Expired/rejected tokens produce "credentials expired, paste new ones" with an edit
130
+ link. The **account selector** in the top bar picks the active account: snapshots, IP list,
131
+ Visual and suggestions are scoped to it; rules are global unless given an account scope. An
132
+ existing single-account database is migrated into one account automatically.
133
+ 2. **Discovery → Refresh from AWS** – takes a read-only snapshot of VPCs, subnets, ENIs and their
134
+ private IPs (plus Lambda, VPC endpoint and load balancer metadata), and shows the
135
+ **VPC → subnet tree** and the per-subnet **address grid**. The history table can delete
136
+ selected snapshots or **Clear history**; both need `DELETE` typed to confirm, and the latest
137
+ snapshot of every account is always kept.
138
+ Refresh, **Crawl services** and **Terraform sync** run as **background jobs**: a modal shows the
139
+ current step (e.g. `root apps env dev · terraform init`), done / total, elapsed time, a live log
140
+ tail and **Cancel** (checked between steps; a running AWS call or terraform command finishes
141
+ first). Only one job runs per account at a time; reloading the page re-attaches to it. Without
142
+ JavaScript the forms still work and wait for the job to finish.
143
+ 3. **IP List** – one flat table of every private IP in the latest snapshot (IP, subnet, VPC,
144
+ resource type, resource name/ref, ENI, status, **environment**), sorted numerically by address.
145
+ Filter by VPC, subnet, resource type, status, environment or free text; **Export to Excel**
146
+ downloads the current filter as an `.xlsx` (one sheet, header row frozen, autofilter enabled).
147
+ A resource's environment comes from the Terraform repo root × environment that manages it,
148
+ else from the first environment tag (Settings; default `Environment`, `env`, `stage`) on the
149
+ resource, its subnet or its VPC. Both Visual views filter by environment and can group / colour
150
+ by it.
151
+ 4. **Visual** – nested diagram per VPC: VPC box → subnet boxes (CIDR, used/idle/free) → one node
152
+ per resource ENI with its AWS icon, name and IPs. More than 10 nodes of one type in a subnet are
153
+ collapsed into a group node (click to expand). Scroll/drag to zoom and pan; double-click a node
154
+ to open its ENI. VPC/subnet borders and the legend can be toggled (remembered per account).
155
+ Both views share one toolbar (zoom, Fit, Expand / Collapse groups, Reset layout) and a
156
+ **Layout** choice – Grid (default), Hierarchy (dagre), Circle, Concentric, Breadthfirst – that
157
+ keeps every box (VPC, subnet, expanded service group, swimlane) together; the layout and the
158
+ expanded groups are remembered per account and view, dragged node positions per account + VPC
159
+ + view until **Reset layout** (which also leaves focus mode). **Export SVG** / **Export
160
+ draw.io** download the view exactly as shown (draw.io: `mxgraph.aws4` shapes, VPC and subnets
161
+ as containers, edges kept). Data comes from `GET /visual/data.json?vpc=<vpc-id>`.
162
+ The **IP view** is the default; the **Extended view** tab adds regional services (SNS, SQS,
163
+ DynamoDB, EventBridge, S3, API Gateway, Step Functions, Lambda, ECS, Secrets Manager names) and
164
+ external nodes (Transit Gateway, peering, internet via IGW / NAT) around the VPC.
165
+ **Crawl services** (opt-in) reads them; every connection carries evidence, strongest first:
166
+ *observed* (flow logs, X-Ray) > *configured* (subscriptions, event source mappings, targets,
167
+ routes, ...) > *permitted* (IAM policies of Lambda / ECS roles) > *referenced* (environment
168
+ variables). Environment variable values, policy documents and secret values are only matched
169
+ against known resource names / ARNs in memory and never stored; IAM `Resource: "*"` shows a
170
+ *broad access* badge. **Flow logs** (opt-in) shows the estimated Logs Insights scan size for the
171
+ chosen window (default 1 hour) before running, and stores only ENI↔ENI/port aggregates.
172
+ Filters by service and evidence level apply to the diagram and its exports. To stay readable
173
+ the Extended view draws **one line per pair of nodes**, styled by its strongest evidence, with a
174
+ *+N* badge for further evidence lines (all listed when the line is clicked). Only *observed* and
175
+ *configured* are shown by default; *permitted* / *referenced* are opt-in, and the selection is
176
+ remembered per account. Regional services of one type ("SQS queue ×18") and Lambda / ECS ENIs
177
+ of a subnet are collapsible groups; lines between collapsed groups merge into one labelled
178
+ *×count*, drawn wider the more connections it stands for. The Hierarchy layout runs left to right:
179
+ sources (EventBridge, SNS, API Gateway, S3) → compute (Lambda, ECS) → targets, with edges routed
180
+ along dagre's paths. Click a node or search by name / ARN / IP to see only its 1- or 2-hop
181
+ neighbourhood (**Show all** or Esc to return). Group by tag, Terraform root or environment draws
182
+ one swimlane per app. Exports contain exactly what is shown. **Kafka**: Amazon MSK clusters
183
+ (`kafka:ListClustersV2`) and Lambda event source mappings from MSK or self-managed Kafka become
184
+ *Kafka: <cluster or bootstrap host>* nodes. Crawled nodes linked to nothing in the VPC shown are
185
+ listed in the **Not linked** side panel (click one to draw it, or **Show all crawled nodes**).
186
+ Icons and labels are larger in the Extended view, and Fit never zooms out below a readable size.
187
+ 5. **Ownership** – who manages each resource, AWS-first; the first match wins:
188
+ **CloudFormation** stack (`cloudformation:ListStacks` / `ListStackResources`) > **IaC tag** (the
189
+ project / repo tag key) > **Terraform** state (optional enrichment, **off by default**) >
190
+ **CloudTrail** creator (opt-in `cloudtrail:LookupEvents` for resources still unowned: a role /
191
+ user matching a *CI role name pattern* → "IaC (unknown repo)", anyone else → "manual") >
192
+ **unmanaged**. Tags of every resource come from `tag:GetResources` (paginated). The source and
193
+ value are shown in the IP List (column and filter), on ENI pages and in the Visual page's
194
+ **Group by owner / team**. The Ownership page (formerly the Terraform / drift page) counts
195
+ resources per source and lists the unmanaged ones and **tag gaps** (resources missing the
196
+ configured environment / project tag); Terraform sync, drift and "managed elsewhere" markers are
197
+ kept in its collapsed, optional Terraform section. **Settings → Ownership** picks the project /
198
+ repo, environment, team and owner tag keys from dropdowns of the tag keys seen in the account,
199
+ the CI role patterns, and the two optional sources. Only the values of those tag keys are stored
200
+ (other tags by key only); CloudTrail keeps the creating role / user *name*, never its ARN or
201
+ session name. **CloudTrail event history only covers the last 90 days**: older resources stay
202
+ unmanaged; at most 50 unowned resources are looked up per Refresh. With the Terraform
203
+ enrichment off, Refresh no longer re-syncs Terraform repos (the Sync buttons still work).
204
+ 6. Define **Rules** (GUI or YAML import/export) and review **Suggestions**; anything a rule forbids is
205
+ greyed out together with the IPs it would have saved.
206
+ 7. **Logs** shows the application log file.
207
+
208
+ ## Trends
209
+
210
+ **Settings → Accounts** sets a scheduled Refresh per account (off, every hour, every 6 hours,
211
+ every 24 hours); it runs while the app is running. **Settings → Snapshot retention** bounds the
212
+ history: snapshots older than the retention window (default 90 days) are deleted and those older
213
+ than the downsample threshold (default 7 days) are thinned to one per UTC day; each account's
214
+ latest snapshot is always kept. The **Trends** page charts used IPs per subnet and VPC over 7, 30
215
+ or 90 days and fits a linear forecast ("full in ~N days" or "stable/declining", with a confidence
216
+ note below 5 points). Subnets forecast to fill within 30 days get a badge on Discovery.
217
+
218
+ ## Diff
219
+
220
+ **Diff** compares two successful snapshots of the active account (by default the latest and the
221
+ one before). It lists ENIs and IPs added, removed and changed, grouped by resource type, owner and
222
+ environment, with the net IP delta per subnet and the top consumers – the resources whose IP count
223
+ grew the most.
224
+
225
+ ## CIDR planner
226
+
227
+ **CIDR Planner** shows, per VPC CIDR, the subnets, the free blocks between them, how many aligned
228
+ /20 … /28 blocks still fit and a fragmentation score. The **Fit** tool carves requested subnet
229
+ sizes (optionally per AZ) out of the free space. **Secondary CIDR** proposals – or a CIDR you type
230
+ in – are checked against the AWS association restrictions and for overlaps with every network
231
+ IPLens knows about: all accounts' VPCs and routes to Transit Gateways and peering connections. It
232
+ works on stored snapshot data only and never creates anything; a Terraform snippet is shown to
233
+ copy.
234
+
235
+ ## Report
236
+
237
+ **Report** renders one self-contained HTML document per account (inline CSS and SVG, no scripts
238
+ or external assets) to print to PDF or share: summary and at-risk subnets, top IP consumers, the
239
+ used-IP trend, suggestions allowed / blocked by rules, CIDR headroom with a secondary-CIDR
240
+ recommendation and ownership coverage. It can be limited to one environment. The **redact
241
+ identifiers** option replaces account ids, ARNs, IPs / CIDRs and resource ids with stable
242
+ keyed-hash placeholders (e.g. `[ip:3fa2c1d0e4]/24`), so relations inside the report survive
243
+ while the values cannot be recovered.
244
+
245
+ ## Ownership
246
+
247
+ The **Ownership** page shows who manages each resource, first match wins: CloudFormation stack,
248
+ IaC tag, Terraform state (optional, off by default), CloudTrail creator (opt-in, last 90 days
249
+ only) or *unmanaged*. It counts resources per source and lists unmanaged resources and tag gaps.
250
+ Tag keys and CI role patterns are chosen in **Settings → Ownership**. See
251
+ [Using IPLens](#using-iplens) step 5 for details.
252
+
253
+ ## Extended view
254
+
255
+ The **Extended view** tab of Visual adds regional services and external nodes (Transit Gateway,
256
+ peering, internet) around a VPC. **Crawl services** is opt-in and every source is optional: a
257
+ missing permission becomes a crawl warning. Each connection carries its strongest evidence
258
+ (*observed* > *configured* > *permitted* > *referenced*); **Flow logs** is a separate opt-in that
259
+ shows the estimated scan size first and stores only aggregates. See
260
+ [Using IPLens](#using-iplens) step 4 for details.
261
+
262
+ ## Terraform (optional)
263
+
264
+ Terraform is **not required**: ownership is AWS-first, and the Terraform enrichment is off by
265
+ default. When enabled, IPLens discovers roots and environments in local repositories and reads
266
+ their state – straight from S3 for `s3` backends, from the file for local backends, otherwise
267
+ through an allowlisted `terraform init` / `show -json` (never `plan`, `apply` or `state`). The
268
+ state adds ownership, environments, drift (in AWS but not in Terraform and vice versa) and
269
+ "managed elsewhere" markers, shown in the collapsed Terraform section of the Ownership page.
270
+
271
+ ## Read-only by construction
272
+
273
+ Every boto3 client is created through `iplens.aws.AwsGateway`, which registers a botocore
274
+ `before-call` hook that rejects any operation not starting with `Describe`, `List` or `Get`.
275
+ The only exceptions are the CloudWatch Logs query calls of the opt-in flow log analysis
276
+ (`logs:StartQuery`, `logs:GetQueryResults`, `logs:StopQuery`, `logs:FilterLogEvents`); calls that
277
+ return secret values (`secretsmanager:GetSecretValue`, `ssm:GetParameter*`, ...) are refused even
278
+ though they start with `Get`. Minimal IAM permissions:
279
+
280
+ ```
281
+ ec2:DescribeVpcs ec2:DescribeSubnets ec2:DescribeNetworkInterfaces ec2:DescribeVpcEndpoints
282
+ lambda:ListFunctions elasticloadbalancing:DescribeLoadBalancers sts:GetCallerIdentity
283
+ ```
284
+
285
+ Optional: `iam:ListAccountAliases` shows the account alias in the header (without it, IPLens
286
+ shows the account id only, or the *Account display name* set in Settings).
287
+ `elasticloadbalancing:DescribeTargetGroups`, `elasticloadbalancing:DescribeTargetHealth` and
288
+ `ec2:DescribeSecurityGroups` add load balancer → target and security group reference
289
+ connections to the Visual page; without them those connections are left out.
290
+ Ownership: `tag:GetResources`, `cloudformation:ListStacks`, `cloudformation:ListStackResources`
291
+ and, when enabled in Settings, `cloudtrail:LookupEvents` (all read-only and listed explicitly in
292
+ the guard's allowlist); a missing permission becomes a Refresh warning and that source is skipped.
293
+
294
+ Extended view (each source optional; a missing permission or disabled feature becomes a crawl
295
+ warning): `sns:List*`, `sqs:ListQueues`, `sqs:GetQueueAttributes`, `dynamodb:ListTables`,
296
+ `dynamodb:DescribeTable`, `events:List*`, `s3:ListAllMyBuckets`,
297
+ `s3:GetBucketNotification`, `apigateway:GET`, `states:List*`, `states:DescribeStateMachine`,
298
+ `secretsmanager:ListSecrets`, `lambda:ListEventSourceMappings`, `ecs:List*`, `ecs:Describe*`,
299
+ `iam:ListAttachedRolePolicies`, `iam:ListRolePolicies`, `iam:GetRolePolicy`, `iam:GetPolicy`,
300
+ `iam:GetPolicyVersion`, `ec2:DescribeRouteTables`, `ec2:DescribeNetworkAcls`,
301
+ `ec2:DescribeTransitGateway*`, `ec2:GetTransitGatewayRouteTablePropagations`,
302
+ `ec2:DescribeVpcPeeringConnections`, `ec2:DescribeManagedPrefixLists`,
303
+ `route53:ListHostedZones`, `route53:GetHostedZone`, `route53resolver:List*`,
304
+ `config:DescribeConfigurationRecorderStatus`, `config:GetResourceConfigHistory`,
305
+ `resource-explorer-2:GetIndex`, `resource-explorer-2:ListResources`, `xray:GetServiceGraph`,
306
+ `kafka:ListClustersV2` (the MSK list call that covers provisioned and serverless clusters).
307
+ Flow logs: `ec2:DescribeFlowLogs`, `logs:DescribeLogGroups`, `logs:StartQuery`,
308
+ `logs:GetQueryResults`, `logs:StopQuery`.
309
+
310
+ ### AWS permissions per feature
311
+
312
+ All permissions are read-only. **Required** ones are needed for a Refresh; **optional** ones can
313
+ be left out – the feature or source is skipped with a Refresh / crawl warning (or, for opt-in
314
+ sources, simply not enabled).
315
+
316
+ | feature | AWS permissions | required? |
317
+ |---|---|---|
318
+ | Discovery / Refresh (also IP List, Visual IP view, scheduled Refresh) | `sts:GetCallerIdentity`, `ec2:DescribeVpcs`, `ec2:DescribeSubnets`, `ec2:DescribeNetworkInterfaces`, `ec2:DescribeVpcEndpoints`, `lambda:ListFunctions`, `elasticloadbalancing:DescribeLoadBalancers` | **required** |
319
+ | Account alias in the header | `iam:ListAccountAliases` | optional (falls back to account id / display name) |
320
+ | Visual: LB → target and security group connections | `elasticloadbalancing:DescribeTargetGroups`, `elasticloadbalancing:DescribeTargetHealth`, `ec2:DescribeSecurityGroups` | optional (connections left out) |
321
+ | Ownership: tags, CloudFormation | `tag:GetResources`, `cloudformation:ListStacks`, `cloudformation:ListStackResources` | optional (skipped with warning) |
322
+ | Ownership: CloudTrail creator | `cloudtrail:LookupEvents` | optional, **opt-in** in Settings |
323
+ | Extended view: services | `sns:List*`, `sqs:ListQueues`, `sqs:GetQueueAttributes`, `dynamodb:ListTables`, `dynamodb:DescribeTable`, `events:List*`, `s3:ListAllMyBuckets`, `s3:GetBucketNotification`, `apigateway:GET`, `states:List*`, `states:DescribeStateMachine`, `secretsmanager:ListSecrets`, `lambda:ListEventSourceMappings`, `ecs:List*`, `ecs:Describe*`, `kafka:ListClustersV2` | optional, each source skipped with warning |
324
+ | Extended view: IAM evidence | `iam:ListAttachedRolePolicies`, `iam:ListRolePolicies`, `iam:GetRolePolicy`, `iam:GetPolicy`, `iam:GetPolicyVersion` | optional, skipped with warning |
325
+ | Extended view: network | `ec2:DescribeRouteTables`, `ec2:DescribeNetworkAcls`, `ec2:DescribeTransitGateway*`, `ec2:GetTransitGatewayRouteTablePropagations`, `ec2:DescribeVpcPeeringConnections`, `ec2:DescribeManagedPrefixLists`, `route53:ListHostedZones`, `route53:GetHostedZone`, `route53resolver:List*` | optional, skipped with warning |
326
+ | Extended view: AWS Config | `config:DescribeConfigurationRecorderStatus`, `config:GetResourceConfigHistory` | optional, skipped with warning |
327
+ | Extended view: Resource Explorer | `resource-explorer-2:GetIndex`, `resource-explorer-2:ListResources` | optional, skipped with warning |
328
+ | Extended view: X-Ray | `xray:GetServiceGraph` | optional, skipped with warning |
329
+ | Flow logs | `ec2:DescribeFlowLogs`, `logs:DescribeLogGroups`, `logs:StartQuery`, `logs:GetQueryResults`, `logs:StopQuery` | optional, **opt-in** per run |
330
+ | Trends, Diff, CIDR planner, Report | none – computed from stored snapshots | – |
331
+ | Terraform sync | `sts:GetCallerIdentity`; S3 backends: `s3:ListBucket`, `s3:GetObject` on the state object, and `sts:AssumeRole` when the backend names a role; other backends: whatever `terraform init` needs to read their state | optional (enrichment off by default) |
332
+
333
+ Besides the `Describe` / `List` / `Get` prefixes, the guard's explicit allowlist is exactly:
334
+ `logs:StartQuery`, `logs:GetQueryResults`, `logs:StopQuery`, `logs:FilterLogEvents` (CloudWatch
335
+ Logs reads for flow logs), `sts:AssumeRole` (Terraform S3 backends with a role), and the ownership
336
+ sources `tag:GetResources`, `cloudformation:ListStacks`, `cloudformation:ListStackResources` and
337
+ `cloudtrail:LookupEvents`.
338
+
339
+ ## Rules
340
+
341
+ | kind | params | effect |
342
+ |---|---|---|
343
+ | `lambda_vpc_required` | – | blocks "run Lambda outside VPC"; flags non-VPC functions |
344
+ | `internal_only` | `scope`: `load_balancer` / `vpc_endpoint` / `both` | blocks public-path suggestions; flags internet-facing LBs |
345
+ | `subnet_reserved` | `subnet_ids` | blocks any suggestion touching or targeting the subnets |
346
+ | `min_free_pct` | `percent`, optional `subnet_ids` | blocks moves that drop a subnet below the threshold; flags subnets below it |
347
+ | `protected_eni` | `pattern` (regex) | blocks suggestions that remove matching ENIs |
348
+
349
+ ```yaml
350
+ rules:
351
+ - name: keep-20pct-free
352
+ kind: min_free_pct
353
+ enabled: true
354
+ params: {percent: 20, subnet_ids: []}
355
+ ```
356
+
357
+ ## Development
358
+
359
+ ```bash
360
+ .venv/bin/python -m playwright install chromium # once, for the browser test
361
+ .venv/bin/python -m pytest -q
362
+ .venv/bin/ruff check .
363
+ .venv/bin/ruff format --check .
364
+ .venv/bin/python -m build # sdist + wheel into dist/
365
+ ```
366
+
367
+ AWS is mocked with moto in tests; fixtures use placeholder data only.
368
+ `tests/test_visual_browser.py` drives the Visual page in headless Chromium (Playwright's own
369
+ download, `$IPLENS_CHROMIUM`, or `/opt/pw-browsers/chromium`). Locally it is skipped when no
370
+ browser is available; with `IPLENS_REQUIRE_BROWSER=1` (set in CI) that is a failure instead.
371
+
372
+ ### Releasing
373
+
374
+ The version lives only in `pyproject.toml` (`iplens --version` reads the installed package
375
+ metadata). Bump it, merge, then push a matching tag, e.g. `v1.0.0`:
376
+ `.github/workflows/publish.yml` checks that the tag equals the version, runs the tests, builds and
377
+ publishes to PyPI via Trusted Publishing (environment `pypi`).
378
+
379
+ ## Vendored front-end assets
380
+
381
+ The app must work without CDN access, so all browser assets are shipped in the package:
382
+
383
+ | asset | path | source | license |
384
+ |---|---|---|---|
385
+ | cytoscape.js 3.34.3 (`dist/cytoscape.min.js`, unmodified) | `iplens/static/vendor/cytoscape.min.js` | npm package `cytoscape@3.34.3` | MIT – `iplens/static/vendor/cytoscape.LICENSE` |
386
+ | dagre (`dagre.min.js`, unmodified) | `iplens/static/vendor/dagre.min.js` | npm package `dagre` | MIT – `iplens/static/vendor/dagre.LICENSE` |
387
+
388
+ SVG export is rendered server-side (`iplens/diagram.py`, icons embedded as data URIs) rather than
389
+ with the `cytoscape-svg` extension, which is GPL-3.0 licensed, not MIT.
390
+ | AWS Architecture Icons (14 SVGs, unmodified, original file names) | `iplens/static/icons/aws/` | AWS Architecture Icons package, release 2026-07-31 (`Icon-package_07312026`), https://aws.amazon.com/architecture/icons/ | see below |
391
+
392
+ **AWS Architecture Icons license.** The icons are © Amazon Web Services, Inc. or its affiliates.
393
+ The 2026-07-31 icon package ships without a `LICENSE.txt`; its usage terms are those published on
394
+ the AWS Architecture Icons page: *"We allow customers and partners to use these toolkits and assets
395
+ to create architecture diagrams."* IPLens uses them only for that purpose (its architecture diagram
396
+ view). They are not covered by IPLens' own license. If a future package release includes a
397
+ `LICENSE.txt`, place it at `iplens/static/icons/aws/LICENSE.txt` and update this section.
398
+
399
+ To update an icon, take the file from the matching folder of a newer package
400
+ (`Architecture-Service-Icons_*/…/48/`, `Resource-Icons_*/Res_Networking-Content-Delivery/`,
401
+ `Architecture-Group-Icons_*/`) and keep the file name, or update `TYPE_ICONS` / `LB_ICONS` /
402
+ `VPC_ICON` in `iplens/queries.py`.