skip to content

Inspection

Four hosts run in two environments, and one database is in maintenance. An access rule lets a web server reach a database in its own environment unless the database is in maintenance. The inspector loads the hosts, the declared runsIn and maintenance edges, and the rule, which derives reaches edges that no declaration states.

platform-inspector.nix
let
gen = (builtins.getFlake "github:sini/gen").lib.mkGenLibs { };
register = {
host = {
web-1 = { role = "webserver"; };
web-2 = { role = "webserver"; };
db-1 = { role = "database"; };
db-2 = { role = "database"; };
};
env = { prod = { }; dev = { }; };
};
relations = {
runsIn = { web-1 = [ "prod" ]; db-1 = [ "prod" ]; web-2 = [ "dev" ]; db-2 = [ "dev" ]; };
maintenance = { db-2 = [ "dev" ]; };
};
fact = label: src: dst: { head = "${label}:${src}:${dst}"; relata = [ src dst ]; };
reach = web: db: env: {
head = "reaches:${web}:${db}";
label = "reaches";
pos = [ "runsIn:${web}:${env}" "runsIn:${db}:${env}" ];
neg = [ "maintenance:${db}:${env}" ];
relata = [ web db ];
};
declarations = [
(fact "runsIn" "web-1" "prod") (fact "runsIn" "db-1" "prod")
(fact "runsIn" "web-2" "dev") (fact "runsIn" "db-2" "dev")
(fact "maintenance" "db-2" "dev")
(reach "web-1" "db-1" "prod")
(reach "web-2" "db-2" "dev")
];
program = gen.program.program (builtins.attrNames register.host ++ builtins.attrNames register.env) declarations;
model = gen.program.model { inherit program; interpretation = [ ]; prior = null; complete = true; };
in
gen.inspect.mkInspector {
inherit register relations declarations model;
minted = { nodes = { }; edges = [ ]; };
}
Terminal window
$ nix eval --impure --json --file platform-inspector.nix \
--apply 'i: i.query "SELECT src, dst FROM edge WHERE label = '\''reaches'\''"'
[{"dst":"db-1","src":"web-1"}]

web-2 does not reach db-2, because db-2 is in maintenance. The same inspector answers why the one derived edge exists:

Terminal window
$ nix eval --impure --json --file platform-inspector.nix \
--apply 'i: i.why (builtins.toJSON [ "reaches" "web-1" "db-1" ])'
{
"derivations": [{
"fired": [
{"atom": "runsIn:web-1:prod", "sign": "pos", "verdict": "true"},
{"atom": "runsIn:db-1:prod", "sign": "pos", "verdict": "true"},
{"atom": "maintenance:db-1:prod", "sign": "neg", "verdict": "false"}
],
"rule": {
"head": "reaches:web-1:db-1",
"neg": ["maintenance:db-1:prod"],
"pos": ["runsIn:web-1:prod", "runsIn:db-1:prod"]
}
}],
"kind": "rule"
}

kind = "rule" says a rule program produced the edge. An edge you declared by hand answers with kind = "declaration" and the site that declared it instead of derivations.

The rule fired because both positive conditions are true and the negative one is false.

FeatureFunctionsPurpose
Load a graphinspect.mkInspector { register; relations; declarations; model; minted; }One set of facts from nodes, declared edges and solved rules
Read the facts.factsNodes, edges with their origin, tables per kind, and a gen-graph value
Query with SQL.query "SELECT …"Filter, join, sort and limit over edge, reaches and one table per kind
Query with selectors.select selectorThe same filters as data; the result is again a set of facts
Explain.why atomThe rule and conditions behind a derived edge or a reachability row
Render.render.mermaid { } facts, .render.dot { } factsA diagram, with derived edges dashed; options set the direction, shapes, labels and nesting
Explain reusememo.why, whyFor, whyNot, whyNotForWhy an incremental run did or did not recompute a node
Trace values.provenance on module and settings resultsWhich file or layer set each value (see Typed Configuration)

render.mermaid turns the facts into a diagram. Declared edges are solid. Edges a rule derived are dashed. render.dot draws the same picture for graphviz. Both take one closed options set first, and { } is the default picture: direction ("LR"), shapeOf (a node kind to a shape), labelOf (a node to its text) and nest (draw the parent edges as nesting).

Terminal window
$ nix eval --impure --raw --file platform-inspector.nix --apply 'i: i.render.mermaid { } i.facts'
flowchart LR
  n0["dev<br/>env"]
  n1["prod<br/>env"]
  n2["db-1<br/>host"]
  n3["db-2<br/>host"]
  n4["web-1<br/>host"]
  n5["web-2<br/>host"]
  n3 -->|"maintenance"| n0
  n2 -->|"runsIn"| n1
  n3 -->|"runsIn"| n0
  n4 -->|"runsIn"| n1
  n5 -->|"runsIn"| n0
  n4 -.->|"reaches"| n2

reaches is a table with the columns src, dst, via and why. A row says dst is reachable from src along edges labelled via. via = '*' means any label, and every node reaches itself:

Terminal window
$ nix eval --impure --json --file platform-inspector.nix \
--apply 'i: i.query "SELECT dst FROM reaches WHERE src = '\''web-1'\'' AND via = '\''*'\'' ORDER BY dst"'
[{"dst":"db-1"},{"dst":"prod"},{"dst":"web-1"}]

The rule engine computes reachability. Path enumeration is not offered, because a cyclic graph has infinitely many paths. Ask reaches for what a node reaches and why for the edge that carries it.

A query naming a table, column, label or via that does not exist fails with the list of names that do. Without this, WHERE label = 'reach' would return [] and read as “the rule produced nothing”:

gen-inspect: unknown label 'reach'; known: maintenance, runsIn, reaches

gen fails early and names what it expected. Each message below comes from the current code.

Error textCauseFix
gen-graph.dependents: 'edges' is not an option of this door; the options are closed (accepted: 'maxIter')The graph was passed where the options set goesCall dependents { } graph node; see Graphs and Queries
gen-graph.ancestorsOf: required field 'parent' is missingThe accessor record lacks a field this query readsAdd the field, here parent = id: …
gen-merge: a definition for option `ssh.port' is not of the expected type: expected type 'int' but value "22" is of type 'string'A definition has the wrong typeFix the value, or the option’s type
The option `ssh.prot' does not exist. Definition values: followed by - In `/modules/web-1.nix': 1 and Did you mean `ssh.port'?A definition has no matching option, often a typoCorrect the path, or declare the option
gen-schema: ref field 'home' on kind 'admin': reference 'web-3' not found in instance registry (available: db-1, web-1)A reference names an instance the registry does not haveUse one of the listed names, or add the instance
gen-aspects.includes (aspect 'webserver', include position 0): reference 'sshd' names no entry of the registryAn includes entry names no aspectCorrect the name; the position is the index in includes
gen-program: the membership 'primary:db-1' is UNDEFINED — …included was read on an atom whose rules contradict each otherRead flag and handle "U"; see Rules
gen-program: 'a' is not in the frozen set of relata …A rule’s relata names an identifier missing from frozenAdd it to frozen
gen-delivery: project: no node selector — `selectNodes` is required and has no default. …project was called without selectNodesPass selectNodes = values: values.hosts or your registry’s path
gen: flakeModule: `gen.nodeRegistryPath` is required and has no default — … Declared top-level keys: …The flake module has no registry pathSet gen.nodeRegistryPath to one of the listed keys’ registry; see Delivery
gen-inspect: unknown label 'reach'; known: …A query names a label, table or column that does not existUse a name from the list

Nix prints the frames that led to an error above the message, and truncates them by default. The last line starting with error: is gen’s message. Pass --show-trace to see every frame. The first frame naming your own file is usually the line to change.


Provided by gen-inspect (facts, SQL, selectors, why, rendering) and gen-memo (explaining reuse). The error messages come from the library each row names.

palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).