Recipes¶
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.
workspace_status, thendataset_summary: 44 assets, 11 open findings (2 CRITICAL, 5 HIGH), 6 internet-exposed assets, 3 cross-account edges.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
}
top_risks(top=10):jump-nsgandsg-admintie at 16.39 (a CRITICAL finding, internet-reachable, small blast radius). The components explain each score.internet_exposure():bastion(SSH viasg-admin) andjump-vm(RDP viajump-nsg) sort first, each with a sensitive port open and no WAF.attack_paths(): the top path isweb-alb -> web-tg -> web-1 -> app-role -> orders-dbwith score 9.8.lateral_movement_paths()andcross_account_edges(external_only=true): the vendor account999999999999can assumedeploy-roleinshared-services.security_coverage(): Security Hub disabled in111111111111/us-east-1,public-apiexposed without a WAF, four workloads without vulnerability scanning.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.
internet_exposure()listsbastionfirst: SSH open throughsg-admin, no WAF.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
}
]
}
find_paths(source="internet", target="bastion")confirms the route0.0.0.0/0 -> sg-admin -> bastion(flow mode, hop typesSECURITY_GROUP_RULE,SG_ADMITS).findings_for_asset(ref="bastion")returnsf-bastion-imds(IMDSv1, HIGH);findings_for_asset(ref="sg-admin")returns the CRITICAL SSH finding.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
}
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"
}
]
get_finding(finding_id="f-ssh-open")for evidence and remediation, thenincident_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).
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"
}
]
}
dependents(ref="sg-app"):web-1andweb-2depend on the group directly (ATTACHED_TO),tg-webat depth 2 andalb-webat depth 3.dependency_tree(ref="sg-app", direction="down")gives the same set as a tree for the change ticket.shared_dependencies():sg-apphas 2 direct dependents, behindkms-main(4).cross_account_edges(account_id="111111111111")lists the prod account's cross-account edges; of the three in the sample, none involvessg-app.snapshot_dataset(new_name="before-change"), make the change, collect again withmap_inventory(or load the new map), thendiff_datasets(base="before-change").
Cross-account trust review¶
cross_account_edges(external_only=true): one external edge,999999999999 -> 222222222222(IAM_TRUST).get_edges(edge_types=["IAM_TRUST"], cross_account_only=true)lists both trusts intodeploy-role, fromapp-rolein prod and from the vendor account.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
}
findings_for_asset(ref="deploy-role")returnsf-deploy-trust: "Role trusts an external account without ExternalId" (HIGH).organization_topology(include_policies=true): the SCPdeny-unapproved-regionstargets the Workloads OU, which holds both accounts.- The SPARQL equivalent of step 2 is in the cookbook.
Compliance gap triage¶
compliance_summary(framework="PCI")gives the posture (1 control evaluated, failing, 3 open findings, 3 affected assets).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"
}
]
}
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 control1.3.1, sodefinitionis null.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.
list_controls(framework="PCI-DSS", source="ruleset")lists the full control set for coverage questions.
Drift between two snapshots¶
- Before the change:
snapshot_dataset(new_name="baseline"). - After:
load_dataset(path="inventory-after", name="after")(ormap_inventory(name="after")). diff_datasets(base="baseline", target="after"): the result is shown in full underdiff_datasets. On the edited estate it reports the WAF removed, itsPROTECTSedge removed,web-2changed (internet_exposed,tags) and newly exposed, a newINTERNET_EXPOSEDedge to it,f-ssh-openresolved andf-db-backupraised from MEDIUM to HIGH.drift_review(base="baseline", target="after")turns that diff into a review task with the diff embedded.- Follow up with
get_asset(ref="web-2"),internet_exposure()andfind_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):
{
"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:
{
"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:
{
"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"
}
}