STIG Help

DSL cascade

STIG's complete cascade vocabulary is:

factory -> project -> defaults -> repo -> group -> element

Each setting uses only the layers where that setting is declared. Shared phase DSL blocks use the three configurable layers:

defaults -> group -> element

The shared phase-block surface is implemented once in com.stigway.gradle.dsl.OverridableElementDsl. Defaults, groups, and elements extend that class instead of redeclaring parallel copies of element-policy fields and phase blocks.

Shared DSL Surface

These settings are intentionally available with the same names in defaults { }, group("...") { }, and stig { element { } }:

Category

Shared settings

Element policy

placement, elementType, importEnabled, failAt, asil, interfacePointMode, interfacePointLocation

Model checks

modelCheck { context/requirements/design/analysis/tests { enabled } }

Build

nativeBuild { bazel { workspaceDir, target, targetPattern, buildOptions, startupOptions, compdbTargets, interfaceTargets, trackActionInputs } }

P3

provenanceBuild { from, include, exclude }

P4

softwareUnderAnalysis { from, include }

P5

analyzerInput { stubs, interfacePoints, symbolVerification, bitcodeTransforms, ... }

P6

analyzerExecution { }

P7

sourceCodeSelection { }, complexityMetrics { }, functionDescriptions { }

Not every field belongs in this shared base. Identity and ownership fields stay on the level that owns them: an element has group, repo, implementationClassName, implementationNamespace, implementationDir, patches, diagram outputs, and known gaps; a group has from(repo); defaults have only the project fallback repo reference.

The resolver walks the layers in that order. For each parameter, the lowest configured layer wins independently from the other parameters. A child setting for from(...) does not reset inherited include or exclude values.

Assignment presence

Resolution is based on whether a property was assigned, never on whether its value differs from the factory default. These cases are therefore distinct:

  • unset: inherit the parent value;

  • explicitly assigned factory default: override the parent with that value;

  • explicitly assigned empty list: clear the inherited list;

  • explicitly assigned null: clear the inherited nullable value when that property defines null as a concrete value.

DSL properties whose public getter exposes a non-null factory default use CascadingDslProperty. This keeps assignment presence separately while preserving convenient Kotlin DSL reads such as timeoutPerRun == 30.minutes. Mutable list surfaces use the same rule and record clear() even when the local list was already empty.

Fully resolved P5 and P6 values retain a DslValueSource (FACTORY, PROJECT, DEFAULTS, REPO, GROUP, or ELEMENT). Their resolvers require an exact provenance schema: missing and unexpected property keys fail immediately. P6 passes the complete source map into stigAnalysisP6RunAnalyzer as a task input and repeats the exact-schema check before analyzer launch. It also requires the separately logged maxAllowedRamAndSwapMiB.setBy, maxParallelAnalyzerExecutions.setBy, and maxSinksPerSolver.setBy values to match the map. Run-plan generation serializes the resolved positive maxSinksPerSolver as --max-sinks-per-solver=<n> in every IDE-XTaint run-plan command; P6 fails if that durable command, its task input, or the value emitted in schema-v7 analysis.json disagree. An explicit null is a real cascade value and means all configured sinks.

bitcodeTransformRegistry { ... } is deliberately not part of this cascade. It is a project-level definition table keyed by human-readable qualified function name. The cascaded P5 bitcodeTransforms block contains activity transform-rule use(...) entries: bitcodeTransforms += { ... } appends to inherited entries, while bitcodeTransforms = { ... } replaces them.

Transform confirmation belongs to each activity transform-rule use(...) entry, not to the project registry rule definition. The same shared rule can match different exact LLVM symbols in different activities, so each activity records its own monotonic review state and current confirmation fingerprint:

bitcodeTransforms += { use( qualifiedFunctionName = "example::Boundary", confirmation = BitcodeTransformConfirmation.USER_CONFIRMED, confirmationFingerprint = "sha256:<current reviewed evidence fingerprint>", ) }

The states are UNCONFIRMED, AI_REVIEWED, and USER_CONFIRMED. Generated source-code-analysis review JSON supplies copy-ready activity use(...) entries. An AI agent may record AI_REVIEWED only after inspecting sufficient original compiled LLVM and caller evidence; production source/header snapshots are not required when LLVM establishes the behavior. The agent follows Check and align analysis evidence for the activity's staged review and may record USER_CONFIRMED only after the user explicitly accepts that exact rule, match inventory, P5 validation, and fingerprint. Changes to semantics, exact matches, owners, application evidence, or validation evidence invalidate the fingerprint. setBy remains independent: it describes how the applied semantic rule was selected, not who accepted the activity-specific review.

knownGaps { ... } is element-local and does not cascade. It contains only assessed defective logical sink fields via cascadingFault(...) or missingExpectedFlow(...); healthy sinks need no declaration. A current declared gap is SOURCE_CODE/LOW, while a new gap or an invalid, duplicate, stale, or wrong-kind declaration is HIGH in the appropriate source-code or configuration category.

Replacement semantics

Lists use replacement semantics at layer boundaries:

  • unset means "inherit the value from the parent layer";

  • a non-empty list replaces the inherited list;

  • an explicitly assigned empty list clears the inherited list.

Builder methods accumulate only inside the same DSL block. For example, two exclude(...) calls in one group block append to that group's exclude list, but an element-level excludes = listOf(...) replaces the inherited group list.

P5 enable switches are applied after resolution. harness.enabled = true includes explicit harness.includes. When lifecycleProfile is configured, P5 also consumes the outputs of stigGenerateAnalysisHarness directly. stubs.enabled = false keeps the resolved configuration/provenance available for diagnostics but prevents its inherited source files and include paths from reaching compilation tasks and source-code-analysis snapshots.

Example: Native Bazel action tracking

group("source_activities") { nativeBuild { bazel { trackActionInputs = true } } }

trackActionInputs belongs to the Bazel build block, not to element policy. It cascades independently like the other Bazel fields and resolves to true when unset. When enabled, stigAnalysisP1BuildNativeTarget runs the configured native target and writes native_action_inputs_fingerprint.json; source-code-analysis tasks use that file as a Gradle input so Bazel action-input changes invalidate stale analysis outputs. Set it to false only for explicit fast/offline/degraded runs where native source changes should not affect Gradle analysis up-to-date checks.

Example: P3 provenanceBuild

defaults { provenanceBuild { from(nativeBuild.compileCommands) } } group("source_activities") { provenanceBuild { exclude(".*logging.*", ".*tracing.*") } } element { provenanceBuild { includes = emptyList() } }

The element keeps the group excludes while explicitly clearing inherited includes. To remove the inherited excludes for that element too, set excludes = emptyList() in the element block.

Keep this pattern consistent for new phase DSLs: model each inheritable field as nullable in the config object, preserve "unset" through the DSL, and resolve to concrete defaults only at the task boundary.

Task-registration and task-action code must consume a resolve... result, not elementConfig's raw inheritable phase objects. DslCascadeConsistencyTest scans those source areas and fails if a raw analyzerExecutionDsl, analyzerInputDsl, native-build, selection, metrics, or model-check object is read there. Resolver code is the only layer allowed to inspect the raw objects.

Explicitly unsupported compatibility settings

Public settings must not be accepted and then ignored. The current pipeline therefore fails explicitly for:

  • retryOnTimeout = true, until retries have distinct systemd scopes and durable attempt logs;

  • showAllSinksMergeInLeafMode, because v4 emits aggregate ASIL runs independently of analysis depth;

  • analyzerInput.interfacePoints.diagrams, replaced by the per-analysis diagram toggles under analyzerExecution.

analyzerInput.symbolVerification.ignorePatterns is consumed by P6 symbol verification. Plain entries are glob patterns, regex: introduces a regular expression, matches are checked against both demangled and mangled names, and ignored entries are written to allowed_external_symbols.json with category CONFIGURED_IGNORE. Required analyzer entry/source/ sink symbols are never suppressible by this list.

maxAllowedRamAndSwapMiB has two Linux checks: it is compared with MemAvailable + SwapFree before execution, and its value is passed to the transient user scope as MemoryMax=<value>M. MemoryMax is a cgroup memory ceiling; systemd accounts swap separately, and STIG does not set MemorySwapMax. P6 enables scope memory accounting and, after a failed run, records the systemd result, cgroup memory and swap peaks, effective memory ceiling, and any readable matching kernel OOM constraint before cleaning up the scope. SSD-backed swap can delay an OOM, but sustained analyzer working-set traffic through swap has far higher latency than RAM, competes for I/O, and makes elapsed time unstable. It is emergency capacity, not a reason to raise analyzer concurrency.

maxParallelAnalyzerExecutions limits native analyzer processes across all active activity projects in one Gradle build. The factory default is 1. Each P6 activity task uses one shared Gradle service whose maxParallelUsages is the resolved capacity; because run-plan entries inside that task are already serial, this is a strict upper bound. All active activities must resolve the same value, otherwise task configuration fails rather than accepting whichever project registered first. The service does not coordinate separate Gradle invocations. A value greater than one is an explicit opt-in for a host whose RAM, memory bandwidth, and elapsed-time behavior have been measured under that load.

For initialized source analysis, keep root/group/activity provenanceBuild {} blocks empty. Its normal P3 selection is derived from durable softwareUnderAnalysis and each active transform's ownerCompileUnit, not from inherited provenance-build include/exclude lists. The example above remains the cascade contract for manual/non-lifecycle configurations.

Example: P4 analyzer code selection

defaults { softwareUnderAnalysis { from(provenanceBuild.compilationUnits) } } group("source_activities") { softwareUnderAnalysis { requireExplicitIncludes = true } } element { softwareUnderAnalysis { includeTargetSourceTree(repos["sourceRepo"] / "activities/foo") } }

The element resolves to its explicit target source-tree list. Exact include(...) paths and target source trees cascade independently. Set includes = emptyList() or targetSourceTrees = emptyList() to intentionally clear the corresponding inherited selection.

23 October 2026