12
hops across 5 flows in the synthetic end-to-end run
8 of 12
assets refused a category on the first pass, every refusal correct
1
field flip recomputes the whole inventory, exactly once
0
hand-drawn diagrams: every figure is generated from the records
The whole pipeline
Two jobs share one per-client NocoDB base. Nothing runs on a laptop.
Call
Write-back to the base
Both jobs run on the isolated runner container on the VPS, which has no internet egress; neither calls an AI model. Every stage is re-runnable, and every computed field is overwritten on the next run.
1
Capture: the data model
One row per hop, pointing at real inventory rows.
Field names as they appear in the base. Blue fields are written by the scoping engine and overwritten on every run; hand edits do not survive.
Why it is shaped this way
- Hops, not flows. A flow with a multi-link to five systems loses the order, and the diagram generator needs edges. Rows sharing a
Flow Name compose into a path by Step, numbered in tens so a forgotten hop slots in later.
- The regulation's own verbs.
Action is Process / Store / Transmit, the wording the CMMC asset definitions use. The first draft used received / stored / processed / disseminated, and the engine could never have mapped those onto the categories cleanly.
- SPD is a data type. A service that only ever sees logs or configuration is still in scope as a Security Protection Asset. A CUI-only enum would have hidden it.
None hops exist to help prove a system is separated.
- Source equals destination is allowed. A Store hop can point at one asset on both ends. "It comes to rest here" is a different fact from "it moved here".
- Outside parties are free text, not inventory. The prime or a subcontractor goes in
External Endpoint with the link left empty. Anything named there is treated as not the client's, so parking an uninventoried system there would silently corrupt scope. csp_scope.py flags any endpoint receiving CUI or SPD, and any name that looks like a leveraged cloud (GCC High, GovCloud and similar), because a cloud the client controls belongs inside the boundary. A false positive costs a review; a false negative under-scopes.
- Inventory first. A hop can only link to an asset that exists, so the technology inventory (2b) is filled before flows (2c).
How the interview fills it
- Name the flow by the business event, in the "Interview: CUI data flows" view (grouped by flow, ordered by step).
- Walk it forward one hop at a time, setting data type, verb, source and destination on each row.
- Log the boundary crossings explicitly: the first hop in and the last hop out.
- Walk it backward. Backups, logging, monitoring and archives are the hops people forget, and they are the ones that change scope.
An example flow, as rows
| Step | Data Type | Action | Source Technology | Destination Technology | External Endpoint |
| 10 | CUI | Transmit | | Engineering workstation | Prime's supplier portal |
| 20 | CUI | Store | Engineering workstation | File server | |
| 30 | CUI | Process | File server | CAM workstation | |
| 40 | CUI | Transmit | CAM workstation | CNC machine controller | |
| 50 | CUI | Store | File server | Cloud backup service | |
| 60 | SPD | Transmit | File server | Log monitoring (SIEM) | |
Flow Name on every row: "Drawing package from prime to shop floor". Illustrative, not a client. The shaded rows came from the backward walk. Step 40 is CUI because the toolpath is built from the controlled drawing, the same call the synthetic run made.
2
Compute: the scoping engine
Flows plus asset attributes in, one category and a written reason out.
The trigger
- The consultant sets
Run Action to "Recompute scoping" on any Client_Technologies row.
- n8n (
run_scoping_recompute_action) passes the row to the runner, which clears the field before the job runs and confirms the clear by reading it back. The sweep writes back to the same table, so every row it touches re-fires the hook; clearing first means those echoes arrive blank and stop at the guard. Measured: one flip, one recompute-scoping call.
- The runner sweeps the whole inventory, not the one row. Flipping several rows still produces a single run.
Which input decides which category
| Category | Read from | From the flows? |
| Specialized Asset | Specialized Asset Type (multi-select; "Not Specialized" is explicit, so blank means unreviewed) | No |
| CUI Asset | Any Data_Flows hop with Data Type CUI touching the asset, or CUI Handling Intent = Intended | Yes |
| Security Protection Asset | SSP-Named For Requirements, used as the observable stand-in for the security job the asset does | No |
| Contractor Risk Managed | CUI Handling Intent = Capable but not intended, and a linked Restricting Policy | No |
| Out-of-Scope | Intent = Not capable, no CUI hops, no security role, and a Separation Claim backed by Separation Enforced By | Partly |
The original plan was to derive every category from flows. Checking the definitions showed flows settle only two of the five, so the other inputs were added to Client_Technologies the same day (2026-07-20).
Precedence, as the engine runs it
- Specialized Asset, and nothing else. It is tested first and never combines with another category. If the SSP relies on it for a security requirement, that is a design conflict and goes to Needs Review.
- CUI Asset. An asset that handles CUI and also does a security job stays a CUI Asset, the heavier assessment. Its security role is kept in the separate
Security Role field rather than lost (since 2026-09-27).
- Security Protection Asset. Named in the SSP for at least one requirement. A product-category guess ("firewalls are security tools") only produces a "Likely" prompt, never the category.
- Contractor Risk Managed Asset. Only with a linked policy. An unbacked "we don't put CUI there" is not accepted.
- Out-of-Scope. The hardest to earn: every condition must be recorded affirmatively.
- Needs Review. Everything else. The rationale names the missing input.
Fail closed. An empty row can never compute to Out-of-Scope. Missing or contradictory data lands in Needs Review, because a system quietly excluded from an assessment is the expensive way to be wrong. The consultant works the "Scoping: Needs Review (resolve)" view down to zero, and that is what draws the boundary.
Scope reductions stay provisional
Contractor Risk Managed and Out-of-Scope both rest on a control actually working. The engine reads that control's status from the bound gap assessment and moves the asset accordingly.
| Conditioning control | Contractor Risk Managed | Out-of-Scope |
| MET | Stands, confirmed | Stands, confirmed |
| Not yet assessed | Stands, marked provisional | Stands, marked provisional |
| NOT MET | Becomes CUI Asset | Back to Needs Review |
The heavier categories are never reduced by this. Only confirmation or failure of a reducing claim moves a row.
What the output reads like
Every row gets a Scoping Rationale sentence. In the synthetic run the engine caught two classic self-reports, a CNC controller and an office multifunction printer, with:
marked 'capable but not intended', yet CUI actually flows to/from it.
If the flow is real it is a CUI Asset, not a Contractor Risk Managed Asset.
It also caught two of the operator's own entry errors: the file server and CAM workstation marked "Logically separated" while CUI flowed straight to them. The run settled at CUI Asset 5, Security Protection Asset 4, Specialized 1, Out-of-Scope 2, Needs Review 0.
3
Render: generated diagrams
The SSP's figures are built from the same rows, so they can't drift from them.
Three generators, one data pull
| Function | Draws | Reads |
flows_dot() | One data-flow diagram per Flow Name, plus one legend for the section | Hops, and each asset's Scoping Category for node shading |
network_dot() | The boundary: trust zones by category inside a System Boundary box, the perimeter firewall, the internet | Categories, plus SSP naming for 3.13.1 / 3.13.5 to find the boundary device |
ov1_dot() | One-page overview: the enclave, in-scope cloud services inside it, outside parties around it | Only hops that cross the boundary; each party's role is inferred from direction and data type |
What flows_dot() emits for the example
// abridged; ids are row ids, fills are the generator's category colors
digraph flows {
rankdir=LR; bgcolor="white";
node [fontname="Helvetica", fontsize=10, shape=box];
extPrimessupplierportal [label="Prime's supplier portal", style="dashed"];
ct21 [label="Engineering workstation", style="filled,rounded", fillcolor="#f6d0d0"];
ct22 [label="File server", style="filled,rounded", fillcolor="#f6d0d0"];
ct27 [label="Log monitoring (SIEM)", style="filled,rounded", fillcolor="#cfe2f3"];
extPrimessupplierportal -> ct21 [label="CUI Transmit", color="#b22222"];
ct21 -> ct22 [label="CUI Store", color="#b22222"];
ct22 -> ct27 [label="SPD Transmit", color="#d2691e"];
...
}
From dot to Word
generate_ssp.py writes the SSP as Markdown, with each diagram as a fenced dot block.
export_ssp_docx.py finds each block, pipes it to dot -Tpng, and swaps in an image reference. If Graphviz is missing, the block is left as text rather than failing the export.
- pandoc converts GitHub Markdown to .docx against the house reference document.
- The file is attached to the
Gap_Assessments row through the service account. Two jobs: export-ssp-client (clean) and export-ssp-working (adds proposed text for internal review).
The example, drawn
CUI hop
SPD hop
Dashed box: outside party
CUI Asset
Security Protection
Specialized
Redrawn in the Direction B palette for this page. The generator still uses its original colors.
Decision log
The calls that shaped the build, and when they were made.
| Decision | Why | When |
| Model hops, not flows | Diagrams need edges and order | 2026-07-20 |
| Use Process / Store / Transmit | Match the definitions the engine implements | 2026-07-20 |
| Hybrid derivation | Flows settle only CUI Asset and part of Out-of-Scope | 2026-07-20 |
| Clear the trigger before the run | Stops the write-back from re-firing the hook (moved from n8n into the runner, with a read-back check, on 2026-09-17) | 2026-07-20 |
| Separation claim plus enforcing control | Out-of-Scope needs separation, and separation needs proof | 2026-07-22 |
| Two-state coupling to the gap assessment | Scope reductions must survive a NOT MET | 2026-07-22 |
| Generate the diagrams, one per flow | A combined graph was unreadable; hand drawings drift | 2026-07-27 |
| SSP naming is the proxy, not the test | Security Protection status is about function; the SSP records it | 2026-09-12 |
| CUI Asset outranks Security Protection; add Security Role | Keep the heavier assessment without losing the security role | 2026-09-27 |
| Specialized is one category only | An asset accepted as not fully securable can't be what the boundary relies on | 2026-09-29 |
Known gaps
Separation Claim reads the wrong way on a CUI asset. It means "separated from CUI assets", but on a segmented box "Logically separated" reads as true. The operator got it wrong twice in the synthetic run. Fix is wording and option labels, not logic.
- No field for "cannot be fully secured". A Specialized Asset Type alone triggers step 1 of the precedence.
- Hand entry. Phases 2b and 2c are consultant-typed. A client worksheet or portal surface is open, with provenance kept visible and scoping inputs left consultant-only.
- No staleness signal. Nothing detects an inventory that has stopped matching the client's environment.
- Generator colors predate Direction B and will move to the brand tokens when next edited.