Skip to content

Changelog

Notable changes per release. Patch releases are folded into the major entry they belong to.

0.7.0

Released 2026-10-25

Azure and GCP are mapped as deeply as AWS, scanner and collector failures raise exceptions you can import and catch, report.html opens on an animated isometric map of the estate, and the docs site moves to MkDocs Material.

Added

  • Azure depth. Besides the Resource Graph breadth query, cloudg reads the policyresources, securityresources, recoveryservicesresources, networkresources, maintenanceresources, insightsresources and kubernetesconfigurationresources tables, management-group and tenant-scope role assignments, ARM child resources (APIM, Service Bus, Event Hubs, DNS record sets, Front Door, Virtual WAN, Data Factory and Synapse pipelines, AI Search, Azure OpenAI deployments and more), diagnostic settings, Entra ID through Microsoft Graph (users, groups, app registrations, service principals, app-role assignments, OAuth2 grants, directory roles, PIM eligibility) and the objects inside AKS clusters. The ARM type catalog grows from 125 to 278 types. azure.map_entra, azure.child_resources, azure.diagnostic_settings and azure.data_plane turn the extra sources off.
  • GCP depth. 350 Cloud Asset Inventory types now have extractors (123 before), and API sources fill what the inventory does not return: Cloud DNS records, Security Command Center, Cloud Identity groups, IAM deny policies and principal access boundaries, PAM, audit configs, Cloud Monitoring, OS Config, Artifact Analysis, Backup and DR, Apigee, Cloud Scheduler and others. gcp.deep_sources picks which ones run.
  • Azure and GCP record coverage per task family, as AWS does, show "not enabled" placeholders for Defender plans, Sentinel and Security Command Center services, and follow --services / inventory.exclude_services.
  • cloudg.exceptions: CloudGError and its subclasses (ScannerError, ScannerTimeoutError, ScannerNotInstalledError, ScannerExecutionError, ScannerOutputError, ReportParseError, ScannerFailed, CollectionError, CredentialError, ProviderSDKMissingError, ConfigurationError), also exported from cloudg. Every scanner wrapper takes strict=True to raise on the first failure, records each failure in .failures, and has raise_for_errors(). parse_report(path, strict=True) raises ReportParseError for an unreadable report.
  • report.html: an estate map in 3D (provider plates, account and region platforms, network slabs, subnet tiles, a control deck for identity and governance), with every relation drawn as a trace line on a lane of its own, asset selection, tracing from the internet, turning, zoom and pan. The D3 tree and force views stay one toggle away. The page also gains a posture overview, a top-risks list, an Assets tab, CSV export, light and dark themes and print styles. The Anatomy iso-kit and the Hairline kernel (both MIT) ship in cloudg/templates/vendor/ and are always embedded.
  • The docs site is built with MkDocs Material, with API pages generated from the docstrings, tabbed examples, an Examples section, an Exceptions page, a Credits page and a sample report at /sample-report.html.

Changed

  • Credential, SDK and collection failures raise CredentialError, ProviderSDKMissingError and CollectionError. They keep their old bases (RuntimeError, ImportError), so existing except clauses still match. ResilienceError and LiveOperationRejected now derive from CloudGError.
  • The PyPI project page is a short overview (docs/PYPI.md) that links into the docs site. docs/DOCUMENTATION.md stays the full reference that the MCP server serves.

0.6.1

Released 2026-10-10

A security fix for report.html, a round of fixes found while documenting every command and API, and the new docs site. Upgrade if you open reports built from scanner output you did not produce yourself. Several commands now behave the way their help text always said, so check the Changed list if you script cloudg report, cloudg scan or the exit codes.

Security

  • report.html no longer runs markup or script that arrives in scanner output. Finding titles, descriptions, evidence, resource IDs, asset names and compliance framework names are escaped wherever the page writes them, the page header is autoescaped, and data embedded in the page's script can no longer close the script element.
  • A package can no longer take over a built-in scanner (such as prowler) by registering a cloudg.scanners entry point with the same name. Built-in names always win and the duplicate is ignored with a warning.

Fixed

  • report.html works offline. Chart.js 4.4.0 and D3 7.9.0 ship with cloudg and are embedded in the page when report.inline_js is true (the default). With inline_js: false the page loads the same pinned builds from cdn.jsdelivr.net with Subresource Integrity. Clicking a row in a filtered findings table now opens that finding.
  • Config keys that were accepted but ignored now take effect: graph.persist_graphml, graph.export_cytoscape, graph.max_nodes_warn, rag.chunk_strategy, rag.max_chunk_tokens, report.formats, report.inline_js, report.output_dir (as the default of the CloudGEngine methods), rulesets.load_external and ontology.include_raw_metadata. A misspelt key in any section is logged as Unknown config key <section>.<key> is ignored, not only under ratelimit. rulesets.rules_dir: null loads the packaged rulesets instead of failing validation.
  • cloudg deps --json writes only JSON to stdout. Whenever --json is used, and for cloudg mcp, the banner and logs go to stderr.
  • cloudg report -i keeps the scan ID, timestamps, compliance results and edges of a findings.json, rebuilds edges for older files that lack them, and accepts raw-findings.json.
  • scanners.timeout_seconds limits each scanner process in cloudg run, cloudg scan and CloudGEngine.scan(). Before, the limits were hard-coded at 3600 and 1800 seconds. A scanner that times out is reported as failed and its partial findings are kept.
  • cloudg scan reads -c config.yaml, no longer scans the current directory unless asked, runs the IAM linter when given --assets, and exits 1 when no requested scanner could run. cloudg collect uses the credentials and regions in config.yaml. cloudg map --sweep follows inventory.tagging_sweep unless the flag is given.
  • Prowler and ScoutSuite get the credentials cloudg was given: session token, the assumed role_arn, web identity and aws.profile from the config, and real regions. AWS_DEFAULT_REGION=ALL is no longer set. Region discovery for --regions all uses the same credentials, and falls back to the 17 regions enabled by default in every account when discovery fails.
  • cloudg run exits 3 when collection failed for every target, including a run with no credentials at all where every service fails on its own, and lists the targets that failed. Partial collection still exits 0, with a warning.
  • --accounts without --role-name (or aws.role_name) is an error instead of mapping the caller's own account once per listed ID. aws.role_arn is assumed first and the member role from there, instead of replacing the member role. An --ou that matches nothing is an error that lists the organization's OUs.
  • Scanner plugins registered under cloudg.scanners run from scanners.enabled and --scanners, and unknown scanner names produce a warning. The IAM linter follows scanners.enabled and --scanners too.
  • CloudGEngine.run_pipeline() passes aws.profile to the scanners, PipelineResult.errors lists collection, scanner and analysis failures as well, on_finding fires as each scanner finishes and receives copies normalisation does not change, and the *_sync wrappers raise a clear error inside a running event loop. CloudGEngine.scan() falls back to a Trivy filesystem scan when no images are set, like the CLI.
  • Prowler compliance control IDs name the check (CIS/iam_root_hardware_mfa_enabled) instead of including the account ID, so one check across several accounts is one control. When findings from two scanners are merged, both scanners' check-ID mappings are kept.
  • Compliance results are no longer all FAIL. A control fails when a finding maps to it and passes when only Prowler passing checks (ASFF PASSED, OCSF PASS) map to it; controls nothing assessed are left out. FindingsNormaliser.normalise() takes passed_checks=, and the Prowler parser returns them on the list it gives back.
  • Prowler OCSF output (*.ocsf.json, the Prowler 4 default) is parsed: status, severity, resource, check ID, compliance and muted findings. Before, every record became an "Unknown Prowler Finding", passing checks included. The MCP loaders accept OCSF instead of refusing it. ScoutSuite documentation URLs no longer show up as compliance frameworks.
  • build_asset_map() and build_compliance_map() match findings to assets the way the ontology and RAG exports do, so a finding no longer attaches to every asset that shares its name. CloudGEngine.map_inventory(findings=...) applies the overlay without output_dir too (result.asset_map, result.compliance_map).
  • GraphML export (cloudg run, InventoryResult.export(), the MCP export) no longer fails on assets without an ARN. RelationshipLinker.link() can be called more than once. inventory-map.json saves the collection coverage records and InventoryResult.load() restores them.
  • Each ontology format gets its own file (.ttl, .jsonld, .rdf, .nt). N-Triples no longer overwrite the Turtle file, and spellings such as jsonld no longer land in ontology.ttl. AnalysisResult.rag_chunk_count is filled in.
  • CloudOntology.query_asset_neighbourhood() works with hops above 1 and with IDs containing : or /, and query() returns None for unbound variables. NACL_RULE edges get the same ontology relations as SECURITY_GROUP_RULE edges.
  • ReachabilityAnalyzer.compute_blast_radius() stops at the internet placeholder instead of following an egress rule out to everything the internet reaches.
  • findings.json metadata timestamps are ISO 8601, and an unset completed_at is null rather than "None".

Changed

  • cloudg report refuses to write findings.json over its own input; pass -o to write elsewhere, or --overwrite. findings.json now has an edges list.
  • cloudg scan --scanners defaults to scanners.enabled instead of prowler,checkov, and cloudg scan -p accepts only aws, azure and gcp.
  • The Terraform recreation goes to <output>/terraform, following -o, unless terraform.output_dir is set to another path. The old default ./reports/terraform counts as unset.
  • cloudg run writes topology-cytoscape.json only with graph.export_cytoscape: true, and skips topology.graphml with graph.persist_graphml: false. run_pipeline() writes the formats in report.formats and now includes topology.svg.
  • RAG chunks are sized by rag.max_chunk_tokens (about 4 characters per token) instead of fixed caps of 30 relations, 10 findings and 50 triples. Long lists end with an "... and N more" line, and community chunks are bounded too.
  • report.html is about 485 KB larger with the libraries embedded.
  • query_asset_neighbourhood() rows for more than one hop are {neighbour, neighbourName, hops}. D3 graph JSON has "arn": "" instead of null for assets without an ARN. cloudg.mcp.state.check_prowler_input was removed.
  • ProwlerScanner takes its newer settings (aws_session_token, aws_regions, timeout_seconds, env) and the access keys as keyword arguments; calls that pass them by keyword work as before. aws_region is now the fifth positional argument.
  • Parts of three large modules moved into their own: the engine result types and collection checks to cloudg.api_results, the rate-limit models to cloudg.config_ratelimit, and the RAG chunk text builders to cloudg.graph.rag_text. Everything public is still importable from cloudg.api, cloudg.config and cloudg.graph.rag_export.

Documentation

  • The handbook at morpheuslord.github.io/cloudg is now a site of separate pages instead of one long docs/index.html. Guides, the CLI, the Python API, the MCP server, the reference documents and this changelog each have their own tab and sidebar. site/build.py builds it: the CLI options tables come from the click definitions, the API pages from the docstrings, the config pages from config.yaml and the CloudGConfig models, and the long docs/*.md references are split into one page per section. Writing rules for new pages are in site/AUTHORING.md.
  • New guides cover installation, the pipeline phase by phase, authentication for each provider, AWS Organizations, scanners and ingest input formats, compliance mapping, reports, the graph, ontology and RAG exports, rate limits, Docker, CI and Python recipes. Code samples are complete scripts, and processes are drawn as diagrams.
  • Search (Ctrl K or /), a dark theme, linked code tabs and copy buttons work without a server. Links to the old index.html#anchor sections redirect to the new pages.

0.6.0

Released 2026-10-09

Two additions and a round of graph fixes. cloudg is now a Model Context Protocol server, so AI agents can query an inventory map, its graph, findings, compliance and ontology under an access and privacy policy. Every cloud API call now goes through a shared rate limiter that slows down when a provider throttles, so an organization-wide map (or an agent asking for one too often) no longer fails or hammers the APIs.

The release also changes what the graph analysis reports for the same input. Reachability follows network-flow edges only, matches ports as numbers and gives findings stable ids; the ontology emits different triples; GraphBuilder merges parallel rule edges; the inventory mapper keeps parallel rules when it deduplicates assets; cloudg.retry.with_retry retries a different set of errors. The modules concerned are public API: cloudg.graph.ports (new), cloudg.graph.builder, cloudg.graph.reachability, cloudg.graph.ontology_rules, the ontology and RAG exporters (cloudg.graph.ontology, cloudg.graph.rag_export), cloudg.inventory.dependencies, cloudg.inventory.mapper and cloudg.retry. Embedders that store reachability findings, count graph edges or query the ontology should read Changed and Fixed below.

Added

  • The MCP layer, cloudg.mcp, built around one class, CloudGMCPLayer. Its default catalog has 74 tools in ten categories (graph 17, inventory 11, findings 10, workspace 8, ontology 6, compliance 5, live 5, privacy 5, meta 4, export 3), 13 resources, 11 resource templates and 11 prompts (security_posture_review, attack_surface_report, blast_radius_assessment, compliance_gap_analysis, incident_triage and six more). The CLI also lists cloudg://metrics. Tools return structured content, resource links and progress notifications; lists are paginated and arguments complete. A list cursor belongs to the query and dataset version that produced it: once the dataset is reloaded or replaced, the old cursor is refused with an invalid-arguments error instead of returning the wrong page.
  • Graph analysis in the MCP catalog uses the same rules as the library. Attack paths and find_paths follow the reachability network-flow hops plus explicit identity pivots (role assumption, IAM trust, grants); internet_exposure and the edge search read Azure * ports and Internet / Any / * sources, GCP all-port rules and AWS ICMP rules, and skip egress rules; find_paths(source="internet") works on an Azure-only dataset. Findings and asset references resolve through AssetIndex, so an ambiguous name is never matched to the first asset that has it, and reachability_findings returns the analyser's deterministic ids.
  • Tool errors do not repeat the caller's value or list close matches in their message. Both are in the error data instead (data.value, data.suggestions), where the privacy policy can transform them.
  • A dependency-free native server (cloudg.mcp.native): stdio, streamable HTTP and the deprecated SSE endpoints, bearer tokens mapped to roles, Origin and Host checks against loopback by default, CORS, /healthz. It serves both protocol eras on the same object: the handshake versions 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25, and the stateless 2026-07-28 (server/discover, subscriptions/listen). HTTPConfig caps sessions per principal (max_sessions_per_principal, 100; at the cap the principal's least recently used idle session is closed) and open connections (max_connections, 512), and gives a client 30 seconds to send its headers (header_timeout) and 120 seconds for the body (body_timeout).
  • Flavors and adapters: --flavor native, sdk (the official SDK's low-level server), fastmcp or auto. register_into(server) mounts the whole catalog into an mcp 2.x MCPServer, an mcp 1.x FastMCP, a low-level Server or a fastmcp.FastMCP (2.x, 3.x, 4.x) next to the host's own tools; build_server() and export_definitions() cover the other cases. Verified with mcp 1.30.0 and 2.3.0 and fastmcp 2.14.7, 3.4.8 and 4.0.11. On the SDK and fastmcp HTTP transports a session id is bound to the principal that opened it.
  • Options objects for embedders: LayerOptions (CloudGMCPLayer(options=...)) and ServeOptions (serve_async(options=...)). The existing keyword arguments still work; an unknown keyword raises TypeError.
  • A tool handler can take a single pydantic arguments model, def tool(ctx, args: Model), instead of one parameter per argument. The input schema and the validation are the same as for the flat form.
  • Content _meta keys cloudg/data and cloudg/render: a resource or prompt that renders structured data as text passes the data itself, so the privacy transforms see it with its keys before it is rendered.
  • Policies: seven built-in profiles (open, standard (default), strict, read_only, airgapped, audit, soc-analyst) plus policy files that extend each other, with per-role rules, sensitivity ceilings, capability denials and per-principal rate limits. Hidden primitives look the same as nonexistent ones. The textual graph and ontology exports (cloudg://graph/{format}, cloudg://ontology/{format}, cloudg://ontology/turtle), sparql_query and subgraph_export are RESTRICTED, because the privacy layer cannot see inside one opaque string; a profile with a confidential ceiling hides them (strict, and soc-analyst for every role except lead).
  • Transforms, composed per principal into input and output pipelines: redact, sanitize (fences attacker-controllable text), project, annotate, substitute, alias, regex_replace, rename_keys, template, guard_secrets (refuses secrets in arguments) and depseudonymize, with built-in detectors for secrets and tokens, private keys, cloud credential ids, ARNs and resource ids, account ids, IP addresses, hostnames and e-mail addresses.
  • TokenVault: format-preserving pseudonyms derived with HMAC-SHA256, stable across restarts with CLOUDG_MCP_VAULT_KEY, and saved when the server shuts down when the policy sets vault.path. A pseudonym an agent passes back as an argument is reversed before the tool runs; reveal_token reverses pseudonyms and the aliases introduced by substitute and alias transforms.
  • Middleware: AuditLogMiddleware (JSONL, argument values hashed), MetricsMiddleware (cloudg://metrics), CachingMiddleware, ConcurrencyLimitMiddleware, RetryMiddleware, and your own async (info, call_next) callables.
  • CLI: cloudg mcp serve | tools | resources | prompts | call | read | config, also installed as the cloudg-mcp console script and runnable as python -m cloudg.mcp. cloudg mcp config prints client configuration for Claude Desktop, Claude Code, Cursor and VS Code. Under cloudg mcp, the banner and UI output go to stderr so stdout stays a clean protocol channel.
  • The MCP workspace reads only files under its allowed roots. With none configured (allowed_roots or CLOUDG_MCP_ALLOWED_ROOTS) they are the working directory and the report directory, and either one is left out when it is / or the home directory. A load error never quotes the file's contents, and the search for report files in a directory is bounded. SPARQL queries stop at the row limit or after 60 seconds, and a slow analysis build on one dataset no longer blocks the others or the workspace status.
  • Live tools (map_inventory, collect_assets, run_scanners, run_pipeline) check the configured credentials before any cloud call (preflight, on by default, at most 25 seconds) and run under a live operation guard: identical concurrent calls share one run, a scope collected in the last 120 seconds is refused with retry_after_seconds and a loaded live dataset that covers the same providers, and live operations are capped at one per scope and two overall. A live operation that runs longer than ratelimit.live_operation_timeout_seconds (default 3600) is cancelled, raises LiveOperationTimeout and frees its scopes. force=true skips the cooldown for the admin and operator roles. Each live tool reports the throttling of its own run. run_scanners refuses container image names that start with -. The new rate_limit_status tool and cloudg://ratelimit resource show the guard and throttling state without calling a cloud API.
  • cloudg.resilience: provider-aware error classification (classify, classify_strict for codes, classes and statuses without message text, is_provider_answer, retry_after, describe_error), adaptive token buckets per provider, account, region and service (per action for EC2) that halve on throttling and recover additively, bulkheads, circuit breakers at the granularity of the rate bucket (per action for EC2, per service elsewhere), call_with_resilience / call_with_resilience_sync / @resilient with decorrelated-jitter backoff that never undercuts Retry-After, a per-call deadline and a per-provider retry budget, per-run telemetry (stats_scope, ResilienceStats), the process-wide governor (get_governor, configure, reset_governor) and LiveOperationGuard with its GuardSettings. The first two retries of every call do not draw on the shared retry budget, so one busy run cannot starve every other call of its retries. A half-open breaker lets one probe through and takes the probe back after one cooldown if it never reports; a provider's non-retryable answer (an AccessDenied, a 404) counts as proof that the service is reachable. Calls refused by an open circuit, and calls given up because the retry budget ran out, do not count as breaker failures. Reconfiguring the limits keeps the breakers, budgets and held concurrency slots of providers whose settings did not change.
  • Provider wiring, with rate limits, concurrency caps, breakers and the shared retry budget enforced in every SDK: botocore event hooks on every AWS session cloudg creates (the collectors' aioboto3 sessions, the base and assumed-role sessions behind STS, Organizations and Control Tower discovery and the organization fan-out); an explicit azure-core RetryPolicy and a per-attempt policy on every Azure client, which also reads the x-ms-ratelimit-remaining-subscription-reads header (pausing the whole subscription) and Resource Graph's x-ms-user-quota-* headers; Resource Graph paging that honours Retry-After and quota resets, without retrying a page again on top of azure-core's own 429 retries; a google-api-core Retry capped per page, and per-page pacing, for Cloud Asset Inventory. An instrumented AWS client that refuses a call because a circuit is open or the retry budget is spent raises an error that is also a botocore ClientError with code Throttling, so existing except ClientError handlers catch it. A cancelled aioboto3 call frees its concurrency slot at once.
  • ratelimit config section (RateLimitConfig, ProviderRateLimitConfig, ServiceRateLimitConfig): master and per-provider switches, rates, bursts, account and provider-wide limits, concurrency, adaptive floor, breaker threshold and cooldown, retries, backoff cap, deadline, retry budget, per-service and per-operation overrides, and the live_* settings of the guard. Unknown keys are logged and ignored. config.yaml ships the section with comments.
  • InventoryResult.throttling: the run's throttling summary (totals, per-scope counters, messages, skipped scopes, tripped breakers, slowed buckets), or None when no API pushed back. It is added to summary and inventory-map.json only when present, and load() reads it back. MultiAccountCollector.resilience_stats holds the last run's stats.
  • cloudg.graph.ports: the port and edge rules that the graph builder, the reachability analysis and the ontology share. parse_port_ranges(), edge_port_ranges() and port_in_ranges() turn port_range strings into numeric ranges; is_internet_source() is the single check for a source that stands for the internet (0.0.0.0/0, ::/0, and the Azure tags Internet, Any and * in any letter case).
  • Network-flow traversal as public API: ReachabilityAnalyzer.flow_hops(), flow_successors() and internet_entry_points(), and cloudg.graph.reachability.network_flow_graph(), so other code walks the network the way the exposure analysis does. finding_id() and asset_key() give the id scheme of reachability findings.
  • cloudg.inventory.dependencies.AssetIndex, the one asset matcher: asset id, ARN or alias, unique name, unique ARN tail, plus two opt-in tiers (arn_parts for ARN resource parts such as db:orders, casefold for names in any letter case). AssetIndex.add() takes extra aliases. DependencyGraph.find, the ontology and the RAG exporter use it.
  • RelationType.CONTAINS, a generic containment relation in the CONTAINMENT group, for 64 relation types in all.

Changed

  • aws.max_retries and aws.retry_mode are honoured. Earlier releases accepted them and then hard-coded adaptive mode with 10 retries, so the defaults behave as before. botocore counts max_retries after the first attempt: the default of 10 allows up to 11 attempts per call.
  • Every cloud API call is paced by default at or below the provider's published limits: AWS 20 rps per service and per EC2 action, Route 53 5, tagging API 5, Azure 20 rps per subscription and 3 rps per user for Resource Graph, GCP 1.5 rps for ListAssets. Per-item fan-out is held to those rates, so maps of AMI-heavy or bucket-heavy accounts can take longer than in 0.5.x. ratelimit.enabled: false turns the pacing off.
  • A service that stays throttled after its retries is recorded in coverage as FAILED or PARTIAL with an error starting throttled: instead of failing the run. AWS collectors that swallowed per-item errors used to report SUCCESS with missing assets when they were throttled; they now report PARTIAL, telling calls that gave up after retries apart from calls an open circuit skipped.
  • cloudg.retry.with_retry keeps its signature and still retries everything it retried in 0.5.x. It now also retries provider throttling identified by error code, exception class or HTTP status (Azure 429s, GCP ResourceExhausted), and never sleeps less than the server's Retry-After, capped at its max_delay. It does not retry cloudg's own resilience errors (an open circuit, a spent retry budget, a missed deadline), a plain HTTP 500 or an error whose message only mentions rate limiting. cloudg.retry re-exports the main cloudg.resilience names.
  • New optional extra mcp (mcp>=1.30,<3). all now includes it, and so does dev. The core layer and its native server need no extra.
  • The wheel ships docs/DOCUMENTATION.md and docs/INVENTORY_REFERENCE.md as cloudg/_docs/, so the MCP cloudg://docs/{topic} resources work from a plain pip install. The sdist includes every docs/*.md.
  • ReachabilityAnalyzer.find_internet_exposed() walks network-flow edges only, so generate_findings() no longer reports resources that are reachable only through an IAM grant. Before, the walk followed every edge type out of an internet-exposed node, including GRANTS_ACCESS, ASSUMES_ROLE, IAM_TRUST and IAM_POLICY_ATTACHMENT, and produced findings such as a CRITICAL "Internet-exposed RDS_INSTANCE" for a database whose only link to the internet was a role granted access to it. The rules, documented in the module docstring:
    • INTERNET_EXPOSED, LOAD_BALANCER_TARGET, ROUTE and PEERING are followed source to target.
    • SECURITY_GROUP_RULE and NACL_RULE are followed source to target, except egress rules (direction: "egress").
    • ATTACHED_TO is followed backwards when the target is a security group, NSG or NACL, or the edge declares PROTECTED_BY_SG / PROTECTED_BY_NACL (traffic a group admits reaches the resources attached to it), and forwards when the source is a NETWORK_INTERFACE or ELASTIC_IP. Other attachments are not followed.
    • CONTAINS is followed only from a VPC, VNET or SUBNET. Organization, account, cluster and namespace containment is not.
    • INVOKES, REFERENCES, USES_IMAGE, LOGS_TO, PROTECTS, MONITORS, MANAGES and GOVERNS are not followed. A Lambda function behind a public API Gateway (an INVOKES edge) is therefore no longer reported as internet-exposed by this analysis.
    • compute_blast_radius() is unchanged and still follows every edge type.
  • Internet entry points are nodes named 0.0.0.0/0 or ::/0 and the source of any non-egress edge whose CIDR is 0.0.0.0/0, ::/0 or an Azure service tag for the internet (Internet, Any, *). Before, every external placeholder node counted as an entry point, including private CIDRs such as 10.0.0.0/8 and unresolved references, and a security group with an egress rule to 0.0.0.0/0 became an entry point itself.
  • Security groups, NSGs, NACLs and target groups are no longer reported as "unexpected internet-exposed resources". The walk still passes through them and marks them is_internet_exposed; a group's open rules are reported by the sensitive-port findings, and a target group's exposure by the findings on the targets behind it.
  • Reachability finding ids are deterministic: a UUID5 hash of the rule and a stable asset key (cloudg.graph.reachability.finding_id(rule, asset_key)) instead of a random uuid4. The key is the asset's ARN, or its asset id when it has no ARN, because collectors give assets a fresh random id on every collection. Exposure findings key on the asset, open-port findings on the keys of the rule's two endpoints and the port, so the same exposure gets the same id on every scan. generate_findings() also returns exposure findings in a stable order.
  • Ontology: CONTAINS edges get a relation only when both endpoint types match it: VPC_CONTAINS_SUBNET (VPC or VNet to subnet), SUBNET_CONTAINS_INSTANCE (subnet to a resource placed in it, not a route table, NACL, gateway or other network construct), CLUSTER_CONTAINS_SERVICE (cluster or Kubernetes namespace to a workload, service, namespace, node group or add-on) and ORG_CONTAINS_ACCOUNT (organization or OU to account). Every other containment (organization to OU, account to VPC, VNet to VM, resource group to resource, unresolved endpoints) uses the new generic RelationType.CONTAINS. Before, these fell back to VPC_CONTAINS_SUBNET and produced triples such as sample-org VPC_CONTAINS_SUBNET Workloads.
  • Ontology: a relationship declared on an edge (NetworkEdge.relationship) replaces the inferred relation on CONTAINS, ATTACHED_TO and the typed inventory edges (INVOKES, USES_IMAGE, ASSUMES_ROLE, GRANTS_ACCESS, LOGS_TO, PROTECTS, MONITORS, MANAGES, GOVERNS, REFERENCES). On SECURITY_GROUP_RULE, IAM_TRUST, IAM_POLICY_ATTACHMENT, LOAD_BALANCER_TARGET, ROUTE, PEERING and INTERNET_EXPOSED edges the declared relation comes first and the inferred ones are added after it.
  • Ontology: SECURITY_GROUP_RULE and NACL_RULE edges no longer yield PROTECTED_BY_SG / PROTECTED_BY_NACL. Those edges point from the traffic source to the group, so they produced triples such as 0.0.0.0/0 PROTECTED_BY_SG sg-web. The protection relations now come from ATTACHED_TO edges whose target is a security group or NSG (PROTECTED_BY_SG) or a NACL (PROTECTED_BY_NACL), so the triple reads web-alb PROTECTED_BY_SG sg-web. Other ATTACHED_TO edges without a declared relationship stay DEPENDS_ON.
  • Ontology: the port relations of a security group rule are read from the parsed port ranges, as the sensitive-port findings are. A rule open on every port (0-65535, protocol ALL / -1 / *, or an Azure * rule for Tcp) is ALL_TRAFFIC; otherwise each of ports 22, 80, 443 and 3389 inside the ranges gives ONLY_SSH, ONLY_HTTP, ONLY_HTTPS or ONLY_RDP (an AWS rule for 1-1024 gives the first three), and a rule that opens none of them is PORT_RESTRICTED. A rule from an Azure Internet, Any or * source is INTERNET_REACHABLE, like one from 0.0.0.0/0.
  • Ontology: ENCRYPTED_BY_KMS now links an encrypted asset to its key: orders-db ENCRYPTED_BY_KMS <key>. Before, infer_relations() added it to every edge whose target had encryption or storage_encrypted plus a kms_key_id in its metadata, so CloudOntology wrote triples with the edge's source as the subject and the encrypted asset as the object, such as ec2-1 ENCRYPTED_BY_KMS rds-1 or subnet-private ENCRYPTED_BY_KMS orders-db. The relation now comes from infer_asset_relations() for the encrypted asset itself. Its kms_key_id is matched against the collected KMS_KEY assets by asset id, ARN, bare key id, alias name or alias ARN. The keys are indexed once per ontology build instead of being scanned for every encrypted asset. If no collected key matches, the object is the raw kms_key_id string. infer_relations() no longer returns ENCRYPTED_BY_KMS unless the edge declares it as its relationship (the linker's REFERENCES edges still do).
  • Ontology: containment inferred from vpc_id metadata points from the VPC to its members. Before, infer_asset_relations() returned (VPC_CONTAINS_SUBNET, vpc) for a subnet and (SUBNET_CONTAINS_INSTANCE, vpc) for anything else in the VPC, which gave subnet VPC_CONTAINS_SUBNET vpc and instance SUBNET_CONTAINS_INSTANCE vpc: the wrong direction, and a subnet relation whose object was a VPC. Now a VPC or VNet asset returns (VPC_CONTAINS_SUBNET, subnet) for each subnet and (CONTAINS, member) for every other asset with the same vpc_id, and member assets return no containment. The return shape of infer_asset_relations() is unchanged: the asset passed in is still the subject of every tuple. If several VPC or VNet assets share a vpc_id, the first one in the list claims the members. CloudOntology.query_asset_neighbourhood() with hops > 1 follows these relations forward, so a walk from a VPC now reaches its members, and a walk from an instance no longer reaches the VPC through them.
  • RAG export: relation-group chunks include the asset-level relations (ENCRYPTED_BY_KMS, vpc_id containment, tag governance, key rotation, EC2 security group membership) next to the edge relations. The per-edge ENCRYPTED_BY_KMS entries described above are gone.
  • GraphBuilder merges parallel rule edges (see Fixed), so graph.number_of_edges() and the edge lists in to_d3_json(), to_cytoscape_json() and GraphML exports shrink by the number of merged rules, and a merged edge's port_range, protocol and description can hold comma- or semicolon-separated lists.

Fixed

  • ReachabilityAnalyzer decided whether a sensitive port was open with a substring test on the edge's port_range string. A rule open on 0-65535, which is how the AWS collector writes protocol -1 (all traffic), produced no SSH or RDP finding, and neither did an Azure NSG rule with * or a comma list such as 443,3380-3390. The same test reported port 22 for 2200-2300 and port 3389 for 33890. The string is now parsed into numeric ranges by cloudg.graph.ports and each of the 11 ports in SENSITIVE_PORTS is tested against those ranges. The module reads a-b, single ports, comma lists and *. An empty port_range on a SECURITY_GROUP_RULE or NACL_RULE edge means every port when the protocol is ALL / -1 / * / Any, or TCP, UDP or SCTP (a GCP firewall protocol with no ports listed). On other edge types an empty string still opens nothing, so a GCP INTERNET_EXPOSED edge for a public bucket gets no port findings. ICMP, ICMPv6, ESP, AH, GRE and IPIP rules open no ports; for ICMP the numbers AWS stores in the port fields are a type and code. Expect more open-port findings on estates with all-traffic or wide-range ingress rules from the internet, and fewer on rules whose range only looked like a sensitive port. An edge still yields at most one finding per port, however many of its ranges overlap that port. The analyzer also reads a ports list from a graph edge when one is present, as before; GraphBuilder still does not copy NetworkEdge.ports onto the graph, since the collectors cap that list at 100 entries and GraphML export cannot store lists.
  • Egress rules (direction: "egress") no longer produce open-port findings. They point from a group to 0.0.0.0/0 and admit nothing inbound. Before this change they were checked like ingress rules and only escaped because the default AWS egress string 0-65535 contains none of the sensitive port numbers as a substring; with numeric matching every security group's default egress rule would otherwise report all 11 ports.
  • Azure NSG rules whose source is the Internet, Any or * service tag now produce open-port findings, and the finding title names the tag. Before, they marked resources internet-exposed but no port finding was ever raised for them.
  • GraphBuilder lost rules that shared a source and target. The graph is an nx.DiGraph, which holds one edge per pair, so the second NetworkEdge for a pair overwrote the first one's attributes. A security group with ingress rules from 0.0.0.0/0 for port 22 and port 443 ended up with a single edge for 443, and ReachabilityAnalyzer never reported SSH open to the internet. An ICMP rule from 0.0.0.0/0 listed after the SSH rule hid it the same way. AWS security groups, Azure NSG rules and GCP firewall rules all produce such pairs. SECURITY_GROUP_RULE, NACL_RULE and INTERNET_EXPOSED edges on the same pair are now merged into one graph edge when their CIDR and direction match:
    • port_range lists the ports of every merged rule, comma-separated and without repeats: rules for 22 and 443 give "22,443".
    • Rules for different protocols are merged too, and protocol lists each one once ("TCP,ICMP"). ICMP, ESP, AH, GRE and IPIP rules add no ports, because the numbers AWS stores for ICMP are a type and code, and Azure writes "*" for an ICMP rule. A TCP, UDP or all-protocol filter rule with no ports (GCP writes these) adds 0-65535. With mixed protocols the ports are a union: the edge shows which ports are open from the source, not which protocol each port is open for. Sensitive-port findings never looked at the protocol, so they come out the same as if each rule had its own edge.
    • description joins the rules' descriptions with "; ".
    • Rules whose CIDR or direction differ, and edges of any other type, still replace the earlier edge on the pair.
  • The inventory mapper's deduplication dropped parallel security group, NACL and internet-exposure rules from inventory-map.json whenever it merged a duplicate asset. Rules that differ in ports, protocol, CIDR or direction are now kept.
  • The ontology and RAG exporters attached findings to assets by raw Finding.resource_id only. Findings whose resource_id is an ARN or a display name, which is common in scanner output, left a dangling resource node (for example cmr:arn:aws:rds:...) with no FINDING_AFFECTS edge to the real asset, and RAG entity and community chunks reported a finding_count of 0 for it. Both exporters now resolve a finding's asset with AssetIndex: by id, then ARN (from resource_id or resource_arn), then unique name, then unique ARN tail, falling back to the raw resource_id only when nothing matches. Ambiguous names never match. DependencyGraph.find behaves as before apart from building its lookup tables once instead of scanning every asset on each call, and an empty reference now returns None.
  • AWS SECURITY_GROUP_RULE edges from collect_edges() now use the security group asset's id as the security group endpoint instead of the native sg-... ID, as Azure NSG rule edges already did. The graph exports no longer contain a disconnected EXTERNAL node per security group, and sensitive-port findings carry the security group's asset ID and ARN. The internet-exposure walk now continues from an open security group to the resources attached to it, so an instance behind a group that admits 0.0.0.0/0 is reported as internet-exposed; before, the walk stopped at the native-ID placeholder. The CIDR endpoint is unchanged.
  • The multi-account collector called STS (AssumeRole, GetCallerIdentity) synchronously on the event loop, which stalled every other collection while it waited. Those calls now run in a worker thread.

Security

  • The MCP layer never echoes a real value that it restored from a pseudonym in the arguments. It records each restored value with its pseudonym, and error messages and error data, results, resource links, progress messages and log notifications are pseudonymised again with those pairs before they leave, for tools, resources, prompts and completions alike. Text payloads (JSON strings, GraphML, N-Triples) are pseudonymised consistently with structured results, using the values the vault already knows (TokenVault.replace_known_values).
  • Progress messages, log notifications, resource-link names, titles and descriptions, and the URIs of embedded resources go through the caller's privacy policy.
  • An unexpected exception in a resource or prompt handler reaches the client as a generic error on every flavor, never as the raw exception text.
  • Percent-encoded and templated resource URIs are put in canonical form before the policy's allow and deny rules are checked, so cloudg://graph/%64%33 is treated as cloudg://graph/d3.
  • DNS rebinding: on a non-loopback bind without --allowed-host, the native HTTP server no longer trusts an Origin just because it equals the Host header. Same-origin requests are accepted only when the Host header is validated.
  • cloudg mcp serve refuses an empty --auth-token or CLOUDG_MCP_AUTH_TOKENS. --registry accepts only a module:attribute dotted name that resolves to a Registry; it loads operator code and must never come from an untrusted source.
  • Session caps, connection caps and read timeouts on the native HTTP server, and session ids bound to the principal on the SDK and fastmcp HTTP transports (see Added).
  • Policy allow and deny lists and rules also match the concrete resource URI that was requested, both as sent and percent-decoded.
  • preview_transform pseudonymises with a separate vault under a random key (Policy.preview_vault()), so a preview cannot confirm which real value is behind a pseudonym. The strict profile hides the tool.
  • privacy_audit_log returns only the caller's own entries unless the caller has the admin or privacy-admin role, and its result says which (scope: own or all).
  • Audit log lines escape CR, LF and other control characters, and a probe for an unknown resource is recorded with a keyed hash of the URI's variable part instead of the value.
  • An invalid policy is reported without echoing the values in it, so a misplaced vault key does not end up in an error message.
  • Vault files are written atomically with fsync, with mode 0600 in directories created with mode 0700.
  • Regular expressions that could backtrack for a long time on hostile input were replaced with linear patterns or small parsers: the vault's token scanner, the detectors for GCP resource names, e-mail addresses, hostnames and AWS secret access keys, the prompt-injection pattern, the RAG member and triple parsers, fence stripping, the explain_* metadata helpers and the literal stripping in sparql_query (a 20,000-character query used to take 23 seconds there; 200,000 characters now take 0.06).
  • Over HTTP with OAuth, the scopes of the access token become the principal's roles as they are, so a token with the scope admin or privacy-admin gets what the policy grants those roles, reveal included. Issue such scopes only to people who may see real identifiers, or pass a principal_resolver that maps scopes onto roles.

Notes

  • Existing imports, signatures and config files keep working, and a config without ratelimit gets the built-in limits. The release is not purely additive, though: the reachability, ontology, graph builder, mapper and with_retry changes under Changed and Fixed alter results for the same input. Reachability finding ids, open-port findings, ontology triples, graph edge counts and the rules kept in inventory-map.json all differ from 0.5.x.
  • Several large modules were split into packages or sibling modules (cloudg.mcp.state and cloudg.mcp.policy are packages, the transforms are spread over smaller modules, cloudg.api moved its scanner runs to cloudg.api_scanners, the linker rules moved to cloudg.inventory.linker_rules), and long functions across the CLI, the collectors, the resilience layer and the MCP server were split up. The documented import paths, public signatures and CLI options are unchanged. The GCP default skip list is now cloudg.config.GCP_DEFAULT_SKIP_ASSET_TYPES; cloudg.collectors.gcp.DEFAULT_SKIP_ASSET_TYPES stays as an alias.
  • The sanitize transform is about three times faster on large results, and the redactor no longer scans the same value twice.
  • The mcp extra allows mcp>=1.30,<3, verified with mcp 1.30.0 and 2.3.0. Mounting into an SDK MCPServer (mcp 2.x) or FastMCP (mcp 1.x) goes through private SDK attributes that a future release can rename, so applications using that path should pin the minor version they tested, for example mcp>=2.3,<2.4. Mounting into the low-level Server and the native server are unaffected.
  • Sessions on the legacy SSE transport of the SDK and fastmcp flavors are not bound to a principal.

Documentation

  • MCP guide: installation, client setup, CLI, transports and protocol versions, flavors, mounting, in-process use, workspace, errors, middleware, live tools, troubleshooting.
  • MCP tool reference: every tool, resource, template and prompt.
  • MCP privacy: profiles, policy schema, transforms, detectors, the token vault.
  • MCP internals: package map, core contracts, the layer, the adapters, the native server.
  • Rate limits and throttling: architecture, provider wiring, built-in limits with sources, every ratelimit key, a throttled run's output, the Python API, tuning, limitations.
  • The feature reference and the handbook have new MCP server and rate limit chapters, the ratelimit configuration and the new Python API. The inventory reference, catalog and internals describe merged rule edges, the port and protocol formats of each provider and the new reachability rules.

0.5.3

Released 2026-10-04

A security fix release for CodeQL's "incomplete URL substring sanitization" alerts (py/incomplete-url-substring-sanitization).

Security

  • The GCP relation extractors decided a resource's API service by testing whether a string started with a host-like literal. The affected checks were the CMEK scan skip for cloudkms.googleapis.com/ asset types, service-account detection on //iam.googleapis.com/ names, and the //sqladmin.googleapis.com/ and //container.googleapis.com/ name normalisation. They now parse the service out of the name with anchored regular expressions, through the new helpers api_service() and asset_type_service() in cloudg.inventory.gcp_relations.names. Behaviour is unchanged; the new code was checked against the previous implementation on about 92,000 generated inputs with no differences.
  • Test assertions that checked a host-like literal against a list with in now use set comparison, so the scanner no longer reads them as URL checks.

0.5.2

Released 2026-10-04

A documentation release: the inventory mapper's return structures, asset types and internals are now documented in full. No code changes.

Documentation

  • Inventory reference: every return structure and exported file of the inventory mapper, field by field, with annotated examples.
  • Inventory catalog: all 156 asset types with the native types mapped to each, their metadata keys, declared relations and the relationship matrix.
  • Inventory internals: architecture, code map, the collector task registry, linker resolution rules, Azure and GCP extractor frameworks, dependency semantics, recipes for extending the mapper, security rules and testing.
  • The feature reference and handbook describe the full AssetType and EdgeType sets and the new models' fields.
  • The handbook changelog lists 0.5.0 and 0.5.1 as separate releases.

0.5.1

Released 2026-10-04

A much deeper inventory map. cloudg map now follows workloads into containers and Kubernetes, through serverless and event wiring, across the IAM graph and the security services watching it all, and across every account of an AWS Organization / Control Tower landing zone. Edges say what they mean, and cloudg deps answers what an asset needs and what breaks with it.

Added

  • Deep AWS service collectors in cloudg.inventory.aws_services, composed into AWSDeepInventoryCollector and grouped into service families selectable with --services / --exclude-services (inventory.services, inventory.exclude_services):
    • containers: ECR repositories (image sample, scan findings summary, registry scan mode, repository policy grants), ECS services and task definitions (images, task and execution roles, secrets, log groups, target groups, subnets), EKS clusters, nodegroups, Fargate profiles, addons, access entries and pod identity associations;
    • Kubernetes: namespaces, Deployments, StatefulSets, DaemonSets, CronJobs, Services, Ingresses and ServiceAccounts read from inside EKS clusters with a read-only EKS token (cloudg.inventory.kubernetes, no new dependency; --kubernetes/--no-kubernetes);
    • serverless and integration: Lambda event source mappings, resource-policy invokers, function URLs, container images, layers, DLQs and EFS mounts; API Gateway REST and HTTP APIs; SQS; SNS subscriptions; EventBridge buses, rules and targets; Step Functions; Kinesis;
    • data, DNS and deployment: deep S3 (real region, notifications, replication, access logging, policy grants), ALB/NLB listeners, certificates and target groups, classic ELB, Auto Scaling groups, launch templates, deep RDS and Aurora, EFS, ElastiCache, OpenSearch, Redshift, Route 53 records, CloudFormation stacks and the resources they manage, log groups, ACM in-use-by, VPC endpoints, flow logs, transit gateway attachments;
    • security services and scanners: GuardDuty, Security Hub, Inspector (per-resource coverage), Macie, AWS Config, IAM Access Analyzer, Detective, WAFv2, Network Firewall, Shield Advanced, CloudTrail. Services that are not enabled appear as enabled: false placeholders;
    • identity: the full IAM graph from GetAccountAuthorizationDetails: trust policies (cross-account, OIDC / IRSA / GitHub), managed policy attachments, group membership, instance profiles, admin detection, and principal → resource grants.
  • AWS Organizations and Control Tower: cloudg map --org (aws.organization config) discovers the OU tree, every member account, SCPs/RCPs and other org policies, delegated administrators and, when present, the Control Tower landing zone (version, drift, governed regions, log archive / audit / config / backup accounts), enabled controls and baselines, then maps every selected account by assuming --org-role (default AWSControlTowerExecution). Filters: --ou, --exclude-account; governed regions replace --regions all. cloudg.inventory.discover_organization() / OrganizationTopology are public.
  • Typed relationships: new EdgeType values INVOKES, USES_IMAGE, ASSUMES_ROLE, GRANTS_ACCESS, LOGS_TO, PROTECTS, MONITORS, MANAGES, GOVERNS; NetworkEdge.relationship (ontology relation name) and NetworkEdge.properties. Collectors declare relationships in metadata["relations"], which the linker resolves.
  • Dependency analysis: cloudg.inventory.DependencyGraph (depends_on, dependents, tree, shared_dependencies, blast_radius), cross-account edges and security coverage (gaps, workloads without vulnerability scanning, internet-facing endpoints without WAF); InventoryResult.dependency_graph(), analysis(), load(); inventory-dependencies.json and inventory-organization.json exports; the cloudg deps command.
  • 47 new AssetType values covering containers, Kubernetes, messaging, data, DNS, IaC, security services and organization structure, mapped to new OWL classes; richer Azure ARM and GCP Cloud Asset Inventory type maps.
  • cloudg map flags: --accounts, --role-name, --org, --org-role, --ou, --exclude-account, --ct-home-region, --services, --exclude-services, --kubernetes/--no-kubernetes.
  • inventory config: services, exclude_services, kubernetes, kubernetes_timeout, iam_resource_edges, max_images_per_repository, stack_resources, account_hierarchy.
  • Coverage expansion after a full audit of every AWS, Azure and GCP service against what cloudg enumerated:
    • AWS breadth: a Cloud Control API sweep lists every resource type with a list handler, tagged or not (the tagging API never returns resources that were never tagged), skipping types a dedicated collector covers. --cloud-control/--no-cloud-control, inventory.cloud_control*.
    • AWS depth, 133 collectors in all (cloudg.inventory.aws_services.{governance,network_ext,application,data_ml}): IAM Identity Center (permission sets, assignments, users, groups, provisioned roles), RAM shares, Service Catalog / Account Factory, StackSets, SAML providers, access key metadata, Roles Anywhere, Cognito, deep KMS / Secrets Manager / DynamoDB, snapshot and AMI sharing, S3 access points and account Block Public Access; VPN, customer and virtual private gateways, transit gateway route tables and peering, PrivateLink services, prefix lists, egress-only gateways, API Gateway authorizers / VPC links / custom domains, deep CloudFront, Route 53 Resolver, Direct Connect, Global Accelerator, listener rules, VPC Lattice, Cloud WAN; CodePipeline / CodeBuild / CodeDeploy / CodeConnections, SSM, CloudWatch alarms, AWS Backup, ECS capacity providers and container instances, Cloud Map, Lambda aliases, Scheduler, Pipes, API destinations, archives, Firehose, log subscriptions and destinations, OAM, Batch, App Runner, Elastic Beanstalk; MSK, Amazon MQ, SageMaker, Bedrock, OpenSearch Serverless, Redshift Serverless, RDS proxies and global clusters, MemoryDB, DAX, Glue, Lake Formation, EMR, Athena, Transfer Family, DataSync, FSx, ECR Public.
    • Azure: collection through Azure Resource Graph with full properties (azure-mgmt-resourcegraph added to the azure extra; SDK fallback without it), typed relationships for managed identities, role assignments, private endpoints, AKS, App Service, Container Apps, networking, Key Vault, storage, SQL and Defender plans; every Enabled subscription; management groups, Azure Landing Zone archetypes and Azure Policy (cloudg.inventory.azure_hierarchy, azure.all_subscriptions, azure.map_management_groups).
    • GCP: collection through Cloud Asset Inventory list_assets with full resource JSON, org-wide when gcp.organization_id is set; typed relationships across compute, firewalls, load balancing, GKE / Workload Identity, Cloud Run, Functions, Pub/Sub, Eventarc, logging sinks, CMEK and Shared VPC (cloudg.inventory.gcp_relations); IAM bindings to service accounts and principal nodes; organization → folder → project hierarchy, organization policies and VPC Service Controls (cloudg.inventory.gcp_hierarchy); new gcp options collection_scope, skip_asset_types, iam_policies, map_hierarchy, org_policies, vpc_service_controls.
    • 62 further AssetType values (identity federation, provisioning, hybrid networking, operations, data processing, ML), 156 in all.
  • Reference catalogs: type maps and skip lists moved out of code into YAML under cloudg/inventory/catalogs/ (aws_cloudcontrol, aws_arn_types, azure_arm_types, gcp_asset_types), validated at load and extensible through $CLOUDG_CATALOG_DIR overlays.

Changed

  • The relationship linker resolves identifiers with account/region scope, normalises image tags, Lambda qualifiers, S3 object ARNs, :* log-group ARNs, assumed-role sessions and DNS names, creates CLOUD_ACCOUNT placeholders (external: true) for references into unmapped accounts, and records unresolved references.
  • Execution roles of Lambda functions and instance profiles are now ASSUMES_ROLE edges (were REFERENCES); security-group attachments carry relationship: PROTECTED_BY_SG.
  • Every account gets a CLOUD_ACCOUNT node containing its top-level resources (inventory.account_hierarchy); the summary adds accounts, relationship counts, cross-account edges, external accounts, security service gaps and unresolved references.
  • AsyncAWSCollector._service_tasks() returns collector callables instead of coroutine objects, so subclasses can prune tasks without leaving unawaited coroutines.
  • MultiAccountCollector passes is_primary_region to collectors that support region scoping, and collects the caller's own account with the base credentials instead of assuming the member role there.
  • GraphML / D3 exports carry account_id on nodes and relationship / description on edges; the ontology uses each edge's declared relationship.
  • AWSDeepInventoryCollector and GCPCollector take their options as keyword arguments validated by DeepInventoryOptions / GCPCollectorOptions: an unknown option name raises TypeError instead of being ignored. tagging_sweep stays the fourth positional parameter of AWSDeepInventoryCollector.
  • Large inventory modules were split into packages and focused modules (aws_services/*, azure_graph/, gcp_relations/, mapper_result, linker_index, aws_deep_tasks, organization_assets); the previous import paths keep re-exporting the same names.
  • URL and host checks in the collectors and the linker (SQS queue URLs, ECR and ACR registry hosts, S3 endpoints) parse the URL and match the host with anchored expressions instead of substring tests.

Fixed

  • Global services (IAM, S3, CloudFront) were collected once per region, so --regions all produced the same role or bucket many times under different IDs. They are now collected once per account, and the mapper deduplicates anything seen twice.
  • Ambiguous names (for example security groups called default) could link to a resource in another account.
  • Per-service collection failures inside an AWS account/region were hidden behind an overall SUCCESS; they are now reported, and the run is marked PARTIAL.
  • AWS: KMS rotation was always reported off; ACM listed only RSA-2048 certificates; SQS stopped at 1,000 queues; organization CloudTrail trails made every member account look unaudited; WAF associations missed App Runner, Verified Access and Amplify; event buses and classic load balancers were not paginated; S3 made one location call per bucket; Network Firewalls attached to transit gateways had no edge; API Gateway stages, S3 access points and WAF IP sets were classified as APIs, buckets and web ACLs.
  • Azure: a single NSG rule using plural address prefixes or application security groups raised during edge collection and discarded the whole subscription; Key Vault details were always empty; only the first subscription was collected (and, with azure-mgmt-resource 26, none could be listed); collection blocked the event loop.
  • GCP: a string state crashed resource mapping and silently truncated every project; IAM members became external placeholder nodes that marked entire projects internet-exposed; firewall edges were fabricated from network tags; subnet containment used the wrong parent; collection blocked the event loop.

0.5.0

Released 2026-09-30

Inventory mapping. A dedicated, scanner-independent function that maps everything deployed (or default) in a cloud estate and how it interlinks. It answers what exists and how it is wired together; the scanners keep answering what is wrong.

Added

  • cloudg map CLI command: deep-collects the complete infrastructure across AWS, Azure and GCP without running any scanner, links it into an interconnected map, and writes inventory-map.json, inventory-map.graphml and inventory-graph.json. --findings merges previously generated scanner findings into asset-map.json (asset → risk) and compliance-map.json (framework → affected assets).
  • cloudg.inventory package, all public API:
    • AWSDeepInventoryCollector adds route tables, internet/NAT gateways, network interfaces, EBS volumes, Elastic IPs, NACLs, VPC peering, transit gateways and customer-managed IAM policies on top of the standard collector, plus a catch-all sweep of the Resource Groups Tagging API so every taggable resource appears on the map.
    • AzureDeepInventoryCollector adds NICs, public IPs, managed disks, load balancers and route tables, plus a full-subscription ARM resources.list() sweep covering every service.
    • GCPDeepInventoryCollector brings a richer Cloud Asset Inventory type taxonomy and IAM policy bindings as attachment edges.
    • RelationshipLinker derives interconnection edges purely from asset metadata (attachment, containment, routing, peering, and a generic cross-service reference pass) and works on any list[CloudAsset].
    • InventoryMapper / InventoryResult: orchestration, summaries, exports, and the build_asset_map() / build_compliance_map() / export_merged() merge helpers.
  • CloudGEngine.map_inventory() / map_inventory_sync() for embedders.
  • inventory: config section (tagging_sweep, link_references).
  • New schema values: AssetType.NETWORK_INTERFACE, EdgeType.ATTACHED_TO, EdgeType.REFERENCES.
  • MultiAccountCollector accepts per-provider collector class overrides.
  • Documentation: inventory mapping is covered in the README, the handbook (cloudg map, the inventory mapping API) and the docs site; a new "deeper toolkit" chapter documents the standalone programmatic APIs (GraphBuilder, CloudOntology, RAGExporter, TerraformExporter, FindingsNormaliser, parsers).

Fixed (pre-release hardening, folded in before publish):

  • The engine's Checkov path passed framework= to a constructor that takes frameworks= (a list) and raised TypeError; it now forwards the configured framework list.
  • The GCP identifier checks in the inventory linker and mapper are anchored regexes, so .googleapis.com inside an unrelated ARN no longer classifies an asset as GCP (CodeQL py/incomplete-url-substring-sanitization).
  • The Pages deploy artifact carries the run attempt in its name; re-running the workflow no longer fails on "Multiple artifacts named github-pages".
  • install.sh no longer uses A && B || C as if-then-else anywhere; every step is an explicit if/else, with a pip_tool helper for the repeated pip installs.
  • Dockerfile pins parliament to 1.6.4.
  • Codacy complexity findings: the long CLI commands (run, scan, ingest), the engine phases (scan, analyze, run_pipeline), the SVG/HTML renderers, the ontology relation inference, the RAG exporters, credentials, registry discovery and the Prowler/IAM-linter scanners were split into focused helpers; behavior is unchanged and the CLI run command no longer takes 27 named parameters.
  • Codacy file-length findings: the four oversized modules were split along their natural seams, with every import path preserved. The CLI helpers moved to cli_helpers.py / cli_run_helpers.py and the run command to cli_commands.py; the AWS per-service collectors moved to aws_services.py / aws_services_extended.py mixins; the ontology relation-inference layer moved to ontology_rules.py (re-exported from ontology); the SVG hierarchical layout moved to svg_layout.py. The SVG output is byte-identical and the whole suite passes unchanged.

0.4.1

Released 2026-09-29

Security hardening pass over the Checkov and Codacy findings.

  • The Docker image now runs as a non-root cloudg user and carries a HEALTHCHECK.
  • install.sh no longer pipes curl output straight into bash; installer scripts are downloaded to a temp file first (Homebrew, NodeSource, Azure CLI).
  • Third-party GitHub Actions are pinned to full commit SHAs across all workflows.
  • Plugin loading validates the module:Class path shape before importlib.import_module.
  • Retry jitter uses the system RNG.
  • Silent except: pass handlers now log at debug level, so swallowed errors are traceable.
  • Log messages no longer put words like "credentials" next to runtime values.
  • The vulnerability-report email moved out of SECURITY.md in favour of GitHub private reporting.
  • A .codacy.yml scopes analysis to shipped code and documents suppressed false positives (pytest asserts, taxonomy names such as AssetType.SECRET).

0.4.0

Released 2026-09-29

Ingest mode. cloudg can now work entirely from scanner outputs you already have, with no cloud credentials and no scanner binaries on the machine.

Added

  • cloudg ingest CLI command. Takes --prowler, --scoutsuite, --checkov and --trivy paths (file or directory, each repeatable, any combination), runs the normaliser and the check-equivalence merge over them, and renders the HTML and JSON reports.
  • parse_report(path) classmethods on ProwlerScanner, ScoutSuiteScanner, CheckovScanner and TrivyScanner. Each parses the tool's native output format directly. Trivy image and filesystem reports are told apart by the ArtifactType field.
  • cloudg.ingest module with parse_report(tool, path) and ingest_reports(mapping).
  • CloudGEngine.ingest_reports(), CloudGEngine.normalise_findings() and CloudGEngine.run_from_reports() / run_from_reports_sync() for embedders.
  • Feature documentation: docs/DOCUMENTATION.md, a rendered handbook at docs/index.html, and this changelog.

Fixed

  • The publish workflow now sets an explicit least-privilege permissions block (CodeQL actions/missing-workflow-permissions).

0.3.0

Released 2026-09-26

The rebrand release: the project, package, CLI, Docker image, installers and CI became cloudg (cloud graphing).

  • Renamed everything to cloudg; classes follow as CloudGConfig and CloudGEngine.
  • Multi-cloud authentication: OIDC web identity federation, STS role assumption with external IDs and multi-account fan-out on AWS; workload identity federation, service principals, managed identity on Azure; credential files and service account impersonation on GCP.
  • Expanded compliance rulesets, generated from Prowler's public compliance data: 28 frameworks, 4,166 controls, 10,236 check mappings across AWS, Azure and GCP.
  • uv-based packaging and a PyPI publish workflow using trusted publishing (OIDC), plus a Docker image on GHCR bundling all four scanner binaries.
  • MIT license and community standards files.

Patch releases folded in

  • 0.3.1 (2026-09-26): the IaC scanners no longer fall back to silently scanning the current working directory; they target configured directories or the Terraform recreation of the live infrastructure, and are skipped otherwise.
  • 0.3.2 (2026-09-28): finding deduplication is keyed on scanner check IDs with the rules/check_equivalence.yaml cross-scanner map, instead of resource and title alone. The CLI moved to a dedicated Rich UI layer.

Before 0.3.0

The pre-rebrand history (April 2026): the async AWS collector grew ECS, DynamoDB, CloudFront, Secrets Manager and KMS support with adaptive retries; scanners moved to parallel execution under a thread pool; the Terraform export renderer and live-streaming Prowler output landed; the Click CLI took shape.