Skip to content

v0.6.0 - MCP Lib + Security

Choose a tag to compare

@morpheuslord morpheuslord released this 09 Oct 15:33
· 66 commits to main since this release
1efbc3d

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.

Full Changelog: v0.5.3...v0.6.0