Skip to content

Recipes

From docs/MCP_TOOLS.md on GitHub

Multi-step workflows an agent (or a script) can follow. Each step names the tool and the arguments; the numbers quoted come from the captured runs on the sample estate shown earlier in this document.

Posture review

The security_posture_review prompt packages this sequence.

  1. workspace_status, then dataset_summary: 44 assets, 11 open findings (2 CRITICAL, 5 HIGH), 6 internet-exposed assets, 3 cross-account edges.
  2. findings_summary(group_by="source_tool") to see where findings come from:

findings_summary returned this structuredContent.

{
  "dataset": "sample",
  "group_by": "source_tool",
  "total": 11,
  "suppressed": 1,
  "groups": {
    "prowler": {
      "total": 5,
      "CRITICAL": 1,
      "HIGH": 2,
      "MEDIUM": 1,
      "INFO": 1
    },
    "scoutsuite": {
      "total": 3,
      "HIGH": 1,
      "CRITICAL": 1,
      "MEDIUM": 1
    },
    "checkov": {
      "total": 1,
      "LOW": 1
    },
    "iam": {
      "total": 1,
      "HIGH": 1
    },
    "trivy": {
      "total": 1,
      "HIGH": 1
    }
  },
  "truncated": false
}
  1. top_risks(top=10): jump-nsg and sg-admin tie at 16.39 (a CRITICAL finding, internet-reachable, small blast radius). The components explain each score.
  2. internet_exposure(): bastion (SSH via sg-admin) and jump-vm (RDP via jump-nsg) sort first, each with a sensitive port open and no WAF.
  3. attack_paths(): the top path is web-alb -> web-tg -> web-1 -> app-role -> orders-db with score 9.8.
  4. lateral_movement_paths() and cross_account_edges(external_only=true): the vendor account 999999999999 can assume deploy-role in shared-services.
  5. security_coverage(): Security Hub disabled in 111111111111/us-east-1, public-api exposed without a WAF, four workloads without vulnerability scanning.
  6. compliance_summary(): CIS-AWS at a 25.0% pass rate with 6 open findings.

Investigate an internet-exposed asset

Start from the exposure list and walk outwards from one entry.

  1. internet_exposure() lists bastion first: SSH open through sg-admin, no WAF.
  2. get_edges(target="sg-admin") shows the rule and what the group is attached to:

get_edges returned this structuredContent.

{
  "dataset": "sample",
  "total": 2,
  "offset": 0,
  "returned": 2,
  "next_cursor": null,
  "truncated": false,
  "items": [
    {
      "id": "e-inet-sgadmin",
      "source": "0.0.0.0/0",
      "source_name": "0.0.0.0/0",
      "target": "sg-admin",
      "target_name": "sg-admin",
      "edge_type": "SECURITY_GROUP_RULE",
      "relationship": null,
      "port_range": "22",
      "protocol": "TCP",
      "cidr": "0.0.0.0/0",
      "ports": [
        22
      ],
      "direction": "ingress"
    },
    {
      "id": "e-bastion-sg",
      "source": "bastion",
      "source_name": "bastion",
      "target": "sg-admin",
      "target_name": "sg-admin",
      "edge_type": "ATTACHED_TO",
      "relationship": null
    }
  ]
}
  1. find_paths(source="internet", target="bastion") confirms the route 0.0.0.0/0 -> sg-admin -> bastion (flow mode, hop types SECURITY_GROUP_RULE, SG_ADMITS).
  2. findings_for_asset(ref="bastion") returns f-bastion-imds (IMDSv1, HIGH); findings_for_asset(ref="sg-admin") returns the CRITICAL SSH finding.
  3. lateral_movement_paths(start="bastion") shows where a compromise goes:

lateral_movement_paths returned this structuredContent.

{
  "dataset": "sample",
  "paths": [
    {
      "length": 2,
      "nodes": [
        {
          "id": "bastion",
          "name": "bastion",
          "type": "EC2",
          "account_id": "111111111111",
          "internet_exposed": true,
          "open_findings": 1,
          "max_severity": "HIGH"
        },
        {
          "id": "admin-role",
          "name": "bastion-admin",
          "type": "IAM_ROLE",
          "account_id": "111111111111",
          "internet_exposed": false,
          "open_findings": 0
        },
        {
          "id": "db-secret",
          "name": "orders-db-credentials",
          "type": "SECRET",
          "account_id": "111111111111",
          "internet_exposed": false,
          "open_findings": 0
        }
      ],
      "edge_types": [
        "ASSUMES_ROLE",
        "GRANTS_ACCESS"
      ],
      "summary": "bastion -> bastion-admin -> orders-db-credentials"
    }
  ],
  "total_found": 1,
  "truncated": false,
  "starting_points": 1
}
  1. blast_radius(ref="bastion"): nothing depends on the bastion, but the network view reaches 4 assets including the database secret and the KMS key:

blast_radius returned the network part of its structuredContent.

{
  "reachable_assets": 4,
  "max_depth": 3,
  "risk_score": 2.0,
  "sensitive_reachable": [
    {
      "id": "admin-role",
      "name": "bastion-admin",
      "type": "IAM_ROLE",
      "provider": "AWS",
      "region": "global",
      "account_id": "111111111111",
      "arn": "arn:aws:iam::111111111111:role/bastion-admin",
      "internet_exposed": false,
      "open_findings": 0,
      "max_severity": null,
      "uri": "cloudg://assets/admin-role"
    },
    {
      "id": "db-secret",
      "name": "orders-db-credentials",
      "type": "SECRET",
      "provider": "AWS",
      "region": "us-east-1",
      "account_id": "111111111111",
      "arn": "arn:aws:secretsmanager:us-east-1:111111111111:secret:orders-db-credentials-AbC",
      "internet_exposed": false,
      "open_findings": 0,
      "max_severity": null,
      "uri": "cloudg://assets/db-secret"
    },
    {
      "id": "kms-main",
      "name": "prod-main-key",
      "type": "KMS_KEY",
      "provider": "AWS",
      "region": "us-east-1",
      "account_id": "111111111111",
      "arn": "arn:aws:kms:us-east-1:111111111111:key/1111-2222",
      "internet_exposed": false,
      "open_findings": 0,
      "max_severity": null,
      "uri": "cloudg://assets/kms-main"
    }
  ]
}

The result also carried these resource links in content:

[
  {
    "type": "resource_link",
    "uri": "cloudg://assets/bastion",
    "name": "bastion",
    "mimeType": "application/json"
  }
]
  1. get_finding(finding_id="f-ssh-open") for evidence and remediation, then incident_triage(finding_id="f-ssh-open") if it should be handled as an incident.

Blast radius before a change

Planned change: remove the PostgreSQL rule from sg-app (the change_impact_analysis prompt with ref="sg-app" drives the same steps).

  1. get_edges(source="sg-app") shows the rule being changed:

get_edges returned this structuredContent.

{
  "dataset": "sample",
  "total": 1,
  "offset": 0,
  "returned": 1,
  "next_cursor": null,
  "truncated": false,
  "items": [
    {
      "id": "e-sgapp-sgdb",
      "source": "sg-app",
      "source_name": "sg-app",
      "target": "sg-db",
      "target_name": "sg-db",
      "edge_type": "SECURITY_GROUP_RULE",
      "relationship": null,
      "port_range": "5432",
      "protocol": "TCP",
      "ports": [
        5432
      ],
      "direction": "ingress"
    }
  ]
}
  1. dependents(ref="sg-app"): web-1 and web-2 depend on the group directly (ATTACHED_TO), tg-web at depth 2 and alb-web at depth 3.
  2. dependency_tree(ref="sg-app", direction="down") gives the same set as a tree for the change ticket.
  3. shared_dependencies(): sg-app has 2 direct dependents, behind kms-main (4).
  4. cross_account_edges(account_id="111111111111") lists the prod account's cross-account edges; of the three in the sample, none involves sg-app.
  5. snapshot_dataset(new_name="before-change"), make the change, collect again with map_inventory (or load the new map), then diff_datasets(base="before-change").

Cross-account trust review

  1. cross_account_edges(external_only=true): one external edge, 999999999999 -> 222222222222 (IAM_TRUST).
  2. get_edges(edge_types=["IAM_TRUST"], cross_account_only=true) lists both trusts into deploy-role, from app-role in prod and from the vendor account.
  3. lateral_movement_paths(start="acct-vendor") shows what the vendor reaches:

lateral_movement_paths returned this structuredContent.

{
  "dataset": "sample",
  "paths": [
    {
      "length": 2,
      "nodes": [
        {
          "id": "acct-vendor",
          "name": "vendor-co",
          "type": "CLOUD_ACCOUNT",
          "account_id": "999999999999",
          "internet_exposed": false,
          "open_findings": 0
        },
        {
          "id": "deploy-role",
          "name": "deploy-role",
          "type": "IAM_ROLE",
          "account_id": "222222222222",
          "internet_exposed": false,
          "open_findings": 1,
          "max_severity": "HIGH"
        },
        "... 1 more"
      ],
      "edge_types": [
        "IAM_TRUST",
        "GRANTS_ACCESS"
      ],
      "summary": "vendor-co -> deploy-role -> shared-artifacts"
    },
    {
      "length": 2,
      "nodes": [
        {
          "id": "acct-vendor",
          "name": "vendor-co",
          "type": "CLOUD_ACCOUNT",
          "account_id": "999999999999",
          "internet_exposed": false,
          "open_findings": 0
        },
        {
          "id": "deploy-role",
          "name": "deploy-role",
          "type": "IAM_ROLE",
          "account_id": "222222222222",
          "internet_exposed": false,
          "open_findings": 1,
          "max_severity": "HIGH"
        },
        "... 1 more"
      ],
      "edge_types": [
        "IAM_TRUST",
        "GRANTS_ACCESS"
      ],
      "summary": "vendor-co -> deploy-role -> app-images"
    }
  ],
  "total_found": 2,
  "truncated": false,
  "starting_points": 1
}
  1. findings_for_asset(ref="deploy-role") returns f-deploy-trust: "Role trusts an external account without ExternalId" (HIGH).
  2. organization_topology(include_policies=true): the SCP deny-unapproved-regions targets the Workloads OU, which holds both accounts.
  3. The SPARQL equivalent of step 2 is in the cookbook.

Compliance gap triage

  1. compliance_summary(framework="PCI") gives the posture (1 control evaluated, failing, 3 open findings, 3 affected assets).
  2. list_controls(framework="PCI", status="FAIL"):

list_controls returned this structuredContent.

{
  "dataset": "sample",
  "source": "dataset",
  "total": 1,
  "offset": 0,
  "returned": 1,
  "next_cursor": null,
  "truncated": false,
  "items": [
    {
      "framework": "PCI-DSS",
      "control_id": "1.3.1",
      "control_title": "Inbound traffic restricted",
      "status": "FAIL",
      "findings": 2,
      "open_findings": 2,
      "max_severity": "CRITICAL"
    }
  ]
}
  1. control_status(framework="PCI-DSS", control_id="1.3.1") returns the two findings behind the control (SSH open, public S3 read) and the assets. The PCI ruleset has no control 1.3.1, so definition is null.
  2. compliance_gaps(framework="PCI-DSS") ranks the assets:

compliance_gaps returned this structuredContent.

{
  "dataset": "sample",
  "framework": "PCI-DSS",
  "total": 3,
  "offset": 0,
  "returned": 3,
  "next_cursor": null,
  "truncated": false,
  "items": [
    {
      "id": "sg-admin",
      "name": "sg-admin",
      "type": "SECURITY_GROUP",
      "provider": "AWS",
      "region": "us-east-1",
      "account_id": "111111111111",
      "arn": "arn:aws:ec2:us-east-1:111111111111:security-group/sg-0admin",
      "internet_exposed": false,
      "open_findings": 1,
      "max_severity": "CRITICAL",
      "uri": "cloudg://assets/sg-admin",
      "frameworks": [
        "PCI-DSS"
      ],
      "failing_controls": [
        "PCI-DSS:1.3.1"
      ],
      "gap_findings": 1,
      "gap_max_severity": "CRITICAL"
    },
    {
      "id": "logs-bucket",
      "name": "prod-logs",
      "type": "S3_BUCKET",
      "provider": "AWS",
      "region": "us-east-1",
      "account_id": "111111111111",
      "arn": "arn:aws:s3:::prod-logs",
      "internet_exposed": true,
      "open_findings": 1,
      "max_severity": "HIGH",
      "uri": "cloudg://assets/logs-bucket",
      "frameworks": [
        "PCI-DSS"
      ],
      "failing_controls": [
        "PCI-DSS:1.3.1"
      ],
      "gap_findings": 1,
      "gap_max_severity": "HIGH"
    },
    {
      "id": "web-1",
      "name": "web-1",
      "type": "EC2",
      "provider": "AWS",
      "region": "us-east-1",
      "account_id": "111111111111",
      "arn": "arn:aws:ec2:us-east-1:111111111111:instance/i-0web1",
      "internet_exposed": false,
      "open_findings": 1,
      "max_severity": "HIGH",
      "uri": "cloudg://assets/web-1",
      "frameworks": [
        "PCI-DSS"
      ],
      "failing_controls": [],
      "gap_findings": 1,
      "gap_max_severity": "HIGH"
    }
  ]
}

web-1 is a gap through its finding's PCI-DSS tag (the CVE) although no compliance result links that finding, hence the empty failing_controls.

  1. list_controls(framework="PCI-DSS", source="ruleset") lists the full control set for coverage questions.

Drift between two snapshots

  1. Before the change: snapshot_dataset(new_name="baseline").
  2. After: load_dataset(path="inventory-after", name="after") (or map_inventory(name="after")).
  3. diff_datasets(base="baseline", target="after"): the result is shown in full under diff_datasets. On the edited estate it reports the WAF removed, its PROTECTS edge removed, web-2 changed (internet_exposed, tags) and newly exposed, a new INTERNET_EXPOSED edge to it, f-ssh-open resolved and f-db-backup raised from MEDIUM to HIGH.
  4. drift_review(base="baseline", target="after") turns that diff into a review task with the diff embedded.
  5. Follow up with get_asset(ref="web-2"), internet_exposure() and find_paths(source="internet", target="web-2") on what changed.

SPARQL cookbook

Every query below was run with sparql_query against the sample estate; each is followed by its result.

Internet-reachable resources (directly exposed, per the ontology):

SELECT ?r ?name WHERE { ?s cmp:INTERNET_REACHABLE ?r . ?r cmp:hasName ?name } ORDER BY ?name
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "r": "cmr:az-nsg",
      "name": "jump-nsg"
    },
    {
      "r": "cmr:logs-bucket",
      "name": "prod-logs"
    },
    {
      "r": "cmr:api-gw",
      "name": "public-api"
    },
    "... 4 more"
  ],
  "returned": 7,
  "truncated": false,
  "variable_kinds": {
    "r": "uri",
    "name": "literal"
  }
}

Assets flagged internet-exposed, with their ontology class:

SELECT ?name ?type WHERE { ?r cmp:isInternetExposed true ; cmp:hasName ?name ; a ?type } ORDER BY ?name
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "name": "bastion",
      "type": "cm:ComputeInstance"
    },
    {
      "name": "jump-vm",
      "type": "cm:ComputeInstance"
    },
    {
      "name": "prod-logs",
      "type": "cm:ObjectStorage"
    },
    "... 3 more"
  ],
  "returned": 6,
  "truncated": false,
  "variable_kinds": {
    "name": "literal",
    "type": "uri"
  }
}

Assets tagged data=pii (tag individuals are named cmr:tag_<key>_<value>):

SELECT ?name ?type WHERE { ?r cmp:TAGGED_WITH cmr:tag_data_pii ; cmp:hasName ?name ; a ?type } ORDER BY ?name
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "name": "orders-db",
      "type": "cm:RelationalDatabase"
    },
    {
      "name": "prod-data",
      "type": "cm:ObjectStorage"
    }
  ],
  "returned": 2,
  "truncated": false,
  "variable_kinds": {
    "name": "literal",
    "type": "uri"
  }
}

Security groups or NSGs that open SSH or RDP:

SELECT ?rel ?sg WHERE { VALUES ?rel { cmp:ONLY_SSH cmp:ONLY_RDP } ?s ?rel ?t . ?t cmp:hasName ?sg }
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "rel": "cmp:ONLY_SSH",
      "sg": "sg-admin"
    },
    {
      "rel": "cmp:ONLY_RDP",
      "sg": "jump-nsg"
    }
  ],
  "returned": 2,
  "truncated": false,
  "variable_kinds": {
    "rel": "uri",
    "sg": "literal"
  }
}

Everything allowed into each security group:

SELECT ?src ?sg WHERE { ?s cmp:INGRESS_ALLOWED ?t . ?t cmp:hasName ?sg . OPTIONAL { ?s cmp:hasName ?src } } ORDER BY ?sg
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "src": "0.0.0.0/0",
      "sg": "jump-nsg"
    },
    {
      "src": "0.0.0.0/0",
      "sg": "sg-admin"
    },
    {
      "src": "sg-app",
      "sg": "sg-db"
    },
    {
      "src": "0.0.0.0/0",
      "sg": "sg-web"
    }
  ],
  "returned": 4,
  "truncated": false,
  "variable_kinds": {
    "src": "literal",
    "sg": "literal"
  }
}

CRITICAL findings and the assets they affect:

SELECT ?fname ?sev ?aname WHERE { ?f cmp:FINDING_AFFECTS ?a ; cmp:hasName ?fname ; cmp:hasSeverity ?sev . ?a cmp:hasName ?aname . FILTER(?sev = "CRITICAL") }
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "fname": "Security group allows SSH (22) from 0.0.0.0/0",
      "sev": "CRITICAL",
      "aname": "sg-admin"
    },
    {
      "fname": "NSG allows RDP (3389) from Internet",
      "sev": "CRITICAL",
      "aname": "jump-nsg"
    }
  ],
  "returned": 2,
  "truncated": false,
  "variable_kinds": {
    "fname": "literal",
    "sev": "literal",
    "aname": "literal"
  }
}

Where every finding points; the null names are the findings that name their resource by ARN or display name instead of asset id (see Ontology):

SELECT ?f ?target ?name WHERE { ?f cmp:FINDING_AFFECTS ?target . OPTIONAL { ?target cmp:hasName ?name } } ORDER BY ?f
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "f": "cmr:finding_f-bastion-imds",
      "target": "cmr:bastion",
      "name": "bastion"
    },
    {
      "f": "cmr:finding_f-db-backup",
      "target": "cmr:orders-db",
      "name": "orders-db"
    },
    {
      "f": "cmr:finding_f-default-vpc",
      "target": "cmr:vpc-prod",
      "name": "prod-vpc"
    },
    "... 9 more"
  ],
  "returned": 12,
  "truncated": false,
  "variable_kinds": {
    "f": "uri",
    "target": "uri",
    "name": "literal"
  }
}

Cross-account trust:

SELECT ?src ?tgt WHERE { ?s cmp:CROSS_ACCOUNT_TRUST ?t . ?s cmp:hasName ?src . ?t cmp:hasName ?tgt }
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "src": "app-role",
      "tgt": "deploy-role"
    },
    {
      "src": "vendor-co",
      "tgt": "deploy-role"
    }
  ],
  "returned": 2,
  "truncated": false,
  "variable_kinds": {
    "src": "literal",
    "tgt": "literal"
  }
}

Resources encrypted with one KMS key:

SELECT ?name WHERE { ?r cmp:ENCRYPTED_BY_KMS ?k . ?k cmp:hasName "prod-main-key" . ?r cmp:hasName ?name } ORDER BY ?name
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "name": "api-handler"
    },
    {
      "name": "orders-db"
    },
    {
      "name": "orders-db-credentials"
    },
    "... 1 more"
  ],
  "returned": 4,
  "truncated": false,
  "variable_kinds": {
    "name": "literal"
  }
}

The most common classes and properties:

SELECT ?class (COUNT(?s) AS ?n) WHERE { ?s a ?class . FILTER(STRSTARTS(STR(?class), STR(cm:))) } GROUP BY ?class ORDER BY DESC(?n) LIMIT 5
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "class": "cm:SecurityFinding",
      "n": "12"
    },
    {
      "class": "cm:EdgeMetadata",
      "n": "10"
    },
    {
      "class": "cm:TagValue",
      "n": "6"
    },
    "... 2 more"
  ],
  "returned": 5,
  "truncated": false,
  "variable_kinds": {
    "class": "uri",
    "n": "literal"
  }
}
SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o . FILTER(STRSTARTS(STR(?p), STR(cmp:))) } GROUP BY ?p ORDER BY DESC(?n) LIMIT 8
{
  "dataset": "sample",
  "query_type": "SelectQuery",
  "rows": [
    {
      "p": "cmp:hasName",
      "n": "57"
    },
    {
      "p": "cmp:hasAccountId",
      "n": "44"
    },
    {
      "p": "cmp:isInternetExposed",
      "n": "44"
    },
    "... 5 more"
  ],
  "returned": 8,
  "truncated": false,
  "variable_kinds": {
    "p": "uri",
    "n": "literal"
  }
}

Everything about one individual:

DESCRIBE cmr:orders-db
{
  "dataset": "sample",
  "query_type": "DescribeQuery",
  "rows": [
    {
      "subject": "cmr:orders-db",
      "predicate": "rdf:type",
      "object": "cm:RelationalDatabase"
    },
    {
      "subject": "cmr:orders-db",
      "predicate": "cmp:hasProvider",
      "object": "AWS"
    },
    {
      "subject": "cmr:orders-db",
      "predicate": "cmp:hasARN",
      "object": "arn:aws:rds:us-east-1:111111111111:db:orders-db"
    },
    "... 8 more"
  ],
  "returned": 11,
  "truncated": false,
  "variable_kinds": {
    "subject": "uri",
    "predicate": "uri",
    "object": "term"
  }
}