Capabilities
A capability is a fact about an estate that chant can prove from its graph:
“this instance can be reached on SSH from the internet”, “this role can reach
that table”. People ask questions in those words, and an agent turns one into
a chant search query with a cap: term
(#4375). Each capability is
a rule written as a path pattern, and the path a match took is its evidence.
#4361 settled the rule language and the first vocabulary. It followed a measurement of three candidate languages on one estate written twice, once as a CloudFormation project and once as a Terraform root.
The catalog
Section titled “The catalog”| Capability | Shape | Meaning | Parameters | Depth |
|---|---|---|---|---|
internet-facing | set | its subnet routes 0.0.0.0/0 to an internet gateway | either | |
open-ingress | set | a security group reaching it, directly or through a launch template, admits 0.0.0.0/0 or ::/0 | port?, proto? | declared |
internet-reachable | set | internet-facing and open-ingress, or a Lambda function URL with no auth | port? | either |
attached-to-nothing | set | no member, attachment or route references it: a security group, subnet, policy or internet gateway | either | |
in-default-vpc | set | in the account’s default VPC | either | |
network | edge | host to host through security-group-to-group ingress | declared | |
stores-data | set | a bucket, table, database, queue, file system, secret or SSM parameter | declared | |
holds-secret | set | a secret, an SSM SecureString, or a data store tagged as restricted | declared | |
encrypted-at-rest | set | a database, file system or volume whose encryption attribute is set; buckets, tables, queues and secrets always are | declared | |
publicly-readable | set | a bucket whose policy allows principal * with no public-access block in the way, or a public ACL | declared | |
runs-as | edge | compute to the role it runs as (instance profile, also through a launch template; Lambda role; ECS task role) | declared | |
can-assume | edge | role to role, where the caller’s permission and the target’s trust both allow sts:AssumeRole | declared | |
grants | edge | role to resource, by an identity policy or a resource policy, with no explicit Deny on the same pair | action? | declared |
reaches | edge | can-assume any number of times, then grants | action? | declared |
exfiltrates | edge | internet-reachable compute that runs-as a role that reaches a store that holds-secret | declared | |
blast | edge | what a compromise reaches: runs-as, can-assume and network any number of times, then what each identity is granted | declared |
grants and reaches count as one row of the ruling’s fifteen. Depth says
which observation can supply the facts: declared from source, live from a
live read or a snapshot, either from both. in-default-vpc is live on a
CloudFormation project, since a template can’t declare the default VPC, and
declared on a Terraform root that declares aws_default_vpc.
A set is tested with cap:<name>. An edge is walked with ->[cap=<name>],
or with <-[cap=<name>] to read it in reverse (“what can reach the customer
database” is <-[cap=reaches]). An optional parameter that is left out drops
the test that reads it: cap:open-ingress is any port, and
cap:open-ingress(22) only rules that cover 22.
chant search "cap:internet-reachable(22)" --explainchant search "cap:stores-data <-[cap=reaches]aws_iam_role.app_data"chant search "kind:aws_iam_role ->[cap=grants(dynamodb:GetItem)]kind:aws_dynamodb_table"chant search "kind:aws_security_group cap:attached-to-nothing"chant search "cap:exfiltrates" --explain --jsonWhat the rules do not evaluate
Section titled “What the rules do not evaluate”A rule reads declared references. Each capability names what it leaves out,
and chant search prints that list under every answer that uses it, with a
count where the graph holds the thing left out:
IAM Condition keys, NotAction, NotResource, permission boundaries and
SCPs are not read. Neither is a statement on Resource: "*", which names no
resource, so no edge leads from it. On behold#912’s terralith, eight team roles
allow DynamoDB actions on *; reaches doesn’t find them, and the
not-evaluated line counts them. AWS-managed policies, which a configuration
names by ARN only, have no edges either.
A reference the graph holds but can’t resolve, such as an expression that
names nothing declared, is left out too. A reference inside a CloudFormation
Sub (Sub`${bucket.Arn}/*` ) is the exception: it is read through the
$sub template the graph keeps since
#4373. Network placement
beyond a subnet’s own route table is also outside the rules. That covers
network ACLs and prefix lists, and the load balancers and API gateways in
front of an instance.
An explicit Deny is evaluated. A Deny statement on the same role and
resource cancels the Allow, for the same action when grants is given one.
The rule language
Section titled “The rule language”Rules are path patterns over a lexicon-neutral view of the graph. Their steps
are the edge terms of the query grammar:
an arrow, an optional depth, then what the edge is. Core ships the rules as a
data file, capabilities/aws.paths in @intentius/chant, read at run time.
in-sg(x, g) := x ->{0,1}template ->sg g:SecurityGroupopen-ingress(c; port?, proto?) := c:Instance|Database|Function|Service ->ingress-of r:IngressRule[@world-open, from <= $port, to >= $port, proto in ($proto, "-1")]internet-reachable(c; port?) := c[@internet-facing, @open-ingress($port)] | c:Function <-function :FunctionUrl[authType = "NONE"]grants(r, x; action?) := r:Role ->allows($action) x !<-denies($action) rreaches(r, x; action?) := r:Role ->{0,}can-assume ->grants($action) x| Construct | Meaning |
|---|---|
name(x) := ... | a set: the nodes x the pattern reaches |
name(a, b) := ... | an edge from a to b, carrying the hops that proved it |
name(x; p?, q) | parameters after ;; ? makes one optional |
| ... | another alternative, on its own line |
x:Label|Other[tests] | a node: an optional variable, labels, tests |
->name, <-name, ->(a|b) | a step along a view edge or an edge capability |
->{0,1}name, ->{2}, ->{1,}, ->* | a depth; a rule may start at 0, which a query may not |
!->name node, !<-name node | a negated step: the walk stays put and goes on only when no such edge leads to such a node |
attr = v, !=, in (v, ...), <, <=, >, >= | attribute tests; against a list, any element |
attr ~ "glob" | glob match, either way round, ignoring case (actions ~ "sts:AssumeRole" holds for sts:*) |
attr | present and not false |
@cap, @cap(args) | the node is in a set capability |
!test | any test, negated |
$p | a parameter’s value |
## key: value | catalog metadata for the rule below it: meaning, answers, depth, not-evaluated, param-<name> |
A variable bound earlier closes the pattern when it appears again: the trust
statement in can-assume ends at ->principal a. A rule with no ## meaning
is a helper the catalog doesn’t list. A capability is computed once per graph,
when something first asks for it, so a negated [!@cap] reads a complete set.
Two capabilities that need each other are refused.
The view rules read
Section titled “The view rules read”chant’s graph IR is not a rule surface as it stands: a policy statement or an
inline ingress rule is a property tree inside one node, a Terraform policy is
jsonencode(...) text, Terraform has link resources where CloudFormation has
a direct reference, and every Terraform resource is Terraform::Resource. So
rules read a view built through each lexicon’s mapping:
- a label per kind (
AWS::EC2::Instanceandaws_instanceare bothInstance), and an edge name per referencing attribute (SubnetIdandsubnet_idare bothsubnet); - a
Statementnode per policy statement, joined to its holder bystatementand to what it names byresourceandprincipal; - an
IngressRuleper inline ingress block and aRouteper inline route block; - one
attachedToedge per policy attachment resource; - a
resourceedge for an ARN string that names a declared role, bucket or table by name.
The mapping ships as data in each lexicon: src/capabilities/mapping.json in
@intentius/chant-lexicon-aws and @intentius/chant-lexicon-terraform. The
kinds core’s derived attributes read
are core’s base mapping, so internetFacing and effectiveIngress work on a
graph read without the lexicon loaded, and the mapping extends it. What a
mapping can’t say as data comes as code: the terraform lexicon reads jsonencode(...) policies. Both reach chant search through the
plugin’s capabilityPack.
The two derived attributes are rules too: internetFacing is
internet-facing, and effectiveIngress is the ingress rules of every group
in-sg reaches.
Lexicons
Section titled “Lexicons”A lexicon supplies a capability when its mapping labels every kind one
alternative of the rule needs, or says it can’t with a code. A lexicon with
no mapping supplies none, with lexicon-has-no-mapping. A mapping that lacks
the labels gives capability-not-derivable, naming them.
| Lexicon | Supplies |
|---|---|
aws | all of them |
terraform | all of them, for the AWS provider’s types |
| any other | none (lexicon-has-no-mapping) |
The capabilities document
Section titled “The capabilities document”chant search --capabilities --json prints the catalog as the read
contract’s capabilities document, contract 1, held to
capabilities.schema.json (@intentius/chant/workspace/capabilities.schema.json).
Like every contract document it carries contract, $schema and chant, and
a refusal is error: { code, message } with exit code 1 (usage-invalid,
for a query or another search flag given with --capabilities). Without
--json it prints one line per capability.
{ "$schema": "https://intentius.io/chant/schemas/workspace/capabilities/v1/capabilities.schema.json", "contract": 1, "chant": "0.124.46", "capabilities": [ { "name": "reaches", "meaning": "can-assume any number of times, then grants", "shape": "edge", "params": [{ "name": "action", "optional": true, "meaning": "an IAM action the grant must cover, such as s3:GetObject" }], "search": "->[cap=reaches]", "answers": ["if this role is compromised, what data can it reach"], "depth": "declared", "lexicons": { "aws": "supported", "terraform": "supported", "k8s": { "code": "lexicon-has-no-mapping", "message": "the k8s lexicon ships no capability mapping" } }, "notEvaluated": ["IAM Condition keys", "Resource \"*\""], "uses": ["can-assume", "grants"], "rule": "reaches(r, x; action?) := r:Role ->{0,}can-assume ->grants($action) x" } ], "helpers": [{ "name": "in-sg", "rule": "in-sg(x, g) := x ->{0,1}template ->sg g:SecurityGroup" }]}| Field | Meaning |
|---|---|
capabilities[].name, meaning | the capability and what it says |
shape | set or edge |
params[] | { name, optional, meaning }, in argument order |
search | the term that asks for it: cap:<name> or ->[cap=<name>] |
answers[] | questions it serves, in people’s words |
depth | declared, live or either |
lexicons | for each lexicon the project loads: supported, or { code, message } with lexicon-has-no-mapping, capability-not-derivable or lexicon-not-loaded |
notEvaluated[] | what the rule leaves out |
uses[] | the capabilities and helpers its rule refers to |
rule | the rule as written |
helpers[] | { name, rule } for each rule the capabilities use that the catalog doesn’t list |
templates[] | the investigation templates, each { id, title, question, sources?, inputs, reads, query, note? } |
In a declared workspace, chant workspace search --capabilities --json
prints the same document from the workspace root’s chant, for every lexicon
a chant member loads, since that chant runs the rules over the composed graph
(see workspace search). The
rules are the root chant’s. Each member’s mapping and adapter run under the
member’s own chant, which prints its part of the view
(#4378). The lexicons
support this document states is therefore the root’s installed lexicons. A
member pinned to another lexicon version answers by that version’s mapping. The
reader conformance suite covers it as search --capabilities. chant’s MCP
capabilities tool returns whichever of the two applies where the server
runs, unchanged. An agent reads it once per session.
Investigation templates
Section titled “Investigation templates”The catalog has a second kind of entry since
#4531. An investigation
template is a fixed query about one resource, so that behold’s graph pane
and choudoufu’s investigate run the same query when someone asks the same
question. Where a chant search path pattern can express the question, the
entry’s query has kind: "path" and needs no graph database; it keeps
working when behold’s optional LadybugDB peer is missing. Questions that
cross sources need Cypher, and those entries have kind: "cypher", a
read-only statement that only behold’s pane runs.
| Template | Query | Reads | Question |
|---|---|---|---|
dependents | path, ->*{{node}} | source | what reads this resource, in its own project |
dependencies | path, <-*{{node}} | source | what this resource reads, in its own project |
paths-to-kind | path, {{node}} ->*kind:{{kind}} | source | does this resource reach anything of a given kind |
capability | path, {{node}} cap:{{capability}} | source | does a capability hold for this resource |
drift | path, {{node}} | live | has this resource drifted in an environment |
dependents-all | cypher | session | what reads this resource, from any source loaded |
dependencies-all | cypher | session | what this resource reads, from any source loaded |
ownership | cypher | session | who owns this cloud ID |
same-object | cypher | session | where else the object behind this resource is declared or recorded |
readers | cypher | session | which other estate or root reads this resource |
history | cypher | snapshots | how this resource’s record changed over time |
Each entry’s first input is its resource input, node, which the caller
resolves to one node from whatever identifier it holds, cloud IDs included.
An input of kind term holds one search term (a kind, or a capability call
with its arguments), and an input of kind scope names the environment the
query is read in without appearing in its text. Filling a path template
replaces each {{<input>}} with the input’s value, the node’s id for
{{node}}, and refuses a value that would add a second search term. Cypher
templates never have text spliced in, since the node’s key is bound as
$key and each other input as $<name>. The Reads column above is the
entry’s reads field, and an entry’s optional note says what the template
does not answer.
chant ships the templates for behold and choudoufu to read as data, in the
file @intentius/chant/capabilities/templates.json and in the templates
field of the capabilities document. That field was added within contract 1,
so the capabilities document’s templates are written by chant 0.124.54 and newer.
Evaluation time
Section titled “Evaluation time”Median of 7 runs on an M-series Mac with Node 24. Each run builds the view and then evaluates all fifteen capabilities over the whole graph:
| Estate | IR nodes | View | All capabilities |
|---|---|---|---|
terralith scale 4 with -findings (behold#912) | 202 | 1.7 ms | 4.1 ms |
| terralith scale 4 | 185 | 1.1 ms | 1.9 ms |
| aws-bench, CloudFormation form | 79 | 0.2 ms | 0.3 ms |
| aws-bench, Terraform form | 61 | 0.2 ms | 0.3 ms |