Cross-Repository Workflow Scanning
Workflow scanning is enabled per tenant on request. Extraction runs for everyone, but the part that follows a call into another repository is switched on by KvantumCI for your tenant. Contact support to have it enabled, and see Turning it on for what changes when it is.
Most teams do not write their security controls into every repository. They write them once, in a shared workflow, a pipeline template or a Jenkins shared library, and every other repository calls it.
A scanner that reads only the calling repository sees a workflow that checks out code, calls something, and finishes. It cannot see the SAST scan, the dependency audit or the secret scan that runs inside the thing being called, so it reports the control as missing. The repository is marked down for a control it genuinely runs.
Workflow scanning follows the call. When a repository invokes a shared workflow that KvantumCI has also scanned, the controls that run inside it are credited to the caller, with evidence showing where they were found.
Supported platforms
| Platform | What is followed |
|---|---|
| GitHub Actions | Reusable workflows called with uses: |
| GitLab CI/CD | Configuration pulled in with include: |
| Azure Pipelines | Pipeline templates referenced with template: |
| Jenkins | Shared libraries loaded by a pipeline |
A call is followed from one repository to another within the same platform. A GitLab pipeline that includes a file from another GitLab project is resolved the same way a GitHub workflow calling a reusable workflow in another repository is.
A Jenkinsfile writes @Library('payments-ci@v2'). Which repository that name refers to is decided by
your Jenkins server's global library configuration, which KvantumCI cannot read from your
repositories. The name and version are used exactly as written, so the shared repository has to be
registered under the identity the Jenkinsfile uses, not under its repository name if the two differ.
If a Jenkins caller reports TEMPLATE_NOT_ONBOARDED against a library you have already onboarded,
this mismatch is the first thing to check.
Setting it up
Workflow scanning needs to have scanned the shared workflow before it can credit anything to the repositories that call it. There are three steps, in this order.
1. Add the shared repository as a project item
Add the repository that holds your shared workflows the same way you add any other, described in Create a project. It needs an active integration, exactly like any scanned repository.
2. Mark it as a template repository
On the project item, turn on the template repository flag. This is what tells KvantumCI it may be used to answer questions asked by other repositories. Nothing is shared until you set it, and turning it off later stops it being used, so a repository is never read on another's behalf without your say-so.
The flag registers the branch you have selected. If your callers pin a tag or a commit, that reference needs registering too. There is no screen for it yet, so ask support to add it.
3. Scan the shared repository first
Run a verification on the shared repository before you rely on its results elsewhere. Registering a repository records that it may be used; scanning it is what records what it actually does.
Until that first scan finishes, callers report REF_NOT_ACTUALIZED: the reference is known but has
nothing behind it yet. This is the most common reason a correctly configured setup does not resolve
on the first try.
After that, scan your calling repositories as usual.
Reading the results
A finding that depends on a shared workflow carries an outcome describing what was found at the other end.
| Outcome | Meaning |
|---|---|
| Verified | The control runs in the shared workflow, and the path to it is reachable. The caller is credited. |
| Conditional | The control is there, but reaching it depends on a value KvantumCI cannot decide, such as a variable set outside the pipeline. Neither credited nor penalised. |
| Absent | The shared workflow was read and the control was not found in it. See Limits worth knowing before treating this as final on a very large repository. |
| Unresolved | The call could not be followed. The reason code says why, and what to do about it. |
| Not applicable | The control does not apply to this repository. |
Conditional is not a failure. It means the answer depends on something outside what was scanned, and KvantumCI would rather say so than guess. The evidence panel names the value it could not decide.
Reading the evidence panel
Expand Occurrence details on a workflow-control finding and the panel shows how the answer was reached, not just what it was.
| Field | What it tells you |
|---|---|
| Outcome badge | One of the five outcomes, with the control and rule beside it (for example SCA · rule 15) and a one-line summary underneath |
| Reason | The humanised reason plus its raw code, for example Invocation reachable INVOCATION_REACHABLE. Every outcome carries one, not only the unresolved ones |
| Matched | What was actually found, such as veracode/veracode-sca@v2. Empty when nothing matched |
| Expected | Where it was looked for, as a remote:// reference naming the provider, repository, the exact commit the shared workflow resolved to, and the file. This is the pinned commit the answer was read from, which is what makes it reproducible |
| Callsite, Job | The file and job in your repository that made the call |
| Workspace | Whether the tool runs in the caller's checkout (CALLER) or the template's |
Below those, the call path shows every hop in order: caller workflow, caller job, template workflow, template job, step, and the invocation itself. Reading it left to right is the quickest way to see where an answer came from, and where a chain stopped.
Three collapsed sections hold the rest: Condition and inputs (every gate and input binding considered, and which could not be decided), Template and resolution (the registration and snapshot behind the answer), and Raw JSON (the complete record, useful for support).
Reason codes on a resolved answer
The codes below appear on findings that were assessed. They explain the outcome rather than a failure to reach one, so they need no action.
| Reason | Appears on | Meaning |
|---|---|---|
INVOCATION_REACHABLE | Verified | The control runs at one or more of this caller's callsites |
NO_MATCHING_INVOCATION | Absent | The shared workflow was read and nothing in it invokes the control |
INVOCATION_CONDITION_FALSE | Absent | The invocation exists but a condition guarding it is provably false |
CONDITION_UNKNOWN | Conditional | A condition could not be decided without running the pipeline |
INPUT_UNKNOWN | Conditional | An input value could not be decided without running the pipeline |
When a call cannot be followed
Every unresolved result carries a reason code. Each one has a specific fix.
| Reason | What happened | What to do |
|---|---|---|
TEMPLATE_NOT_ONBOARDED | The repository being called is not usable for this tenant. | Check for a deactivated one first. If the project item or its integration was deactivated, reactivate it and the existing registration works again. Only onboard and register if nothing is there, since re-registering an existing one is refused as a duplicate. |
REF_NOT_REGISTERED | The repository is onboarded, but the branch, tag or commit the caller asks for is not registered. | Register that exact reference. Matching is literal, so v3.0 does not satisfy a caller pinned at v3. See Which reference is the caller asking for? |
REF_NOT_ACTUALIZED | The reference is registered but has never been scanned. | Registering is not enough: scan the shared repository so the branch resolves to a commit. This is step 3 above. |
TEMPLATE_AMBIGUOUS | The reference does not name a host, and more than one onboarded repository matches it. | Qualify the reference in your pipeline, or remove the duplicate onboarding. |
CAPABILITY_METADATA_MISSING | The shared repository was scanned, but that scan recorded nothing usable for this reference. | Scan the shared repository. This self-heals on its next scan and nothing needs changing in the caller. |
REGISTRATION_WITHDRAWN | The template repository flag was turned off, or its integration went inactive. | Restore the flag, or fix the integration. See Inactive integrations and repositories. |
CAPABILITY_SCHEMA_UNSUPPORTED | The stored scan predates the current analysis and cannot be read safely. | Re-scan the shared repository. |
CAPABILITY_SET_INCOMPLETE | The shared repository's stored data was only partly readable. | Re-scan it. |
MAX_DEPTH_EXCEEDED | The chain continues past your tenant's call-depth limit. | Ask for the limit to be raised (up to 8) to follow it further, or flatten the chain. |
CYCLE_DETECTED | The referenced workflows call each other in a loop. | Break the cycle in the templates. Nothing is credited around a loop. |
ASSESSMENT_LIMIT_EXCEEDED | Too many callsites to assess in one scan, so no callsite can support a verdict. See How much is assessed. | Narrow the project's scope. |
MATRIX_DYNAMIC | The calling job's matrix is built at runtime, so its combinations cannot be enumerated. | Declare the matrix statically if you need the call assessed. |
OUT_OF_SCOPE_FOR_REF | The registered reference does not cover the workflow file being called. | Same check as REF_NOT_REGISTERED below: the registered ref exists, but not the one this call needs. |
SNAPSHOT_CONFLICT | The shared repository's data changed while it was being read. | Transient. It resolves on the next scan. |
A reference that cannot be followed never invents a pass. The control is reported as unresolved and the reason is shown, rather than being silently credited or silently failed.
How far calls are followed
Shared workflows often call other shared workflows. KvantumCI follows that chain to a configurable depth, 3 by default, and it can be set between 1 and 8 for your tenant.
Anything beyond the limit is reported rather than dropped, so a chain that runs deeper than the setting is visible instead of appearing absent. Repeated calls to the same workflow are detected and do not loop.
Which reference is the caller asking for?
Both REF_NOT_REGISTERED and OUT_OF_SCOPE_FOR_REF mean the same thing in practice: the shared
repository is onboarded, but not at the point in its history your pipeline is calling. Registering
a repository always registers a specific branch, tag or commit, not the repository as a whole.
Neither re-scanning the caller nor adding the template again will change this. Find the ref your pipeline names, then make sure that is the one registered.
Where the ref is written, per platform:
| Platform | Example | The ref |
|---|---|---|
| GitHub Actions | uses: acme/ci/.github/workflows/build.yml@main | main, everything after @ |
| GitLab CI/CD | include: with project:, ref: v1.4.0, file: | v1.4.0, the ref: key. If omitted, GitLab uses the default branch |
| Azure Pipelines | a repositories: resource with ref: refs/tags/v3 | refs/tags/v3 |
| Jenkins | @Library('payments-ci@v2') | v2, everything after @ in the library string |
Then, depending on what you find:
- The caller names a different branch than the one selected on the template project item. Either point the caller at the registered branch, or change the project item to the branch the caller uses, and re-scan the shared repository.
- The caller pins a tag or a commit. The template flag only registers a branch, so these need registering as well. There is no screen for it yet, so ask support.
- The caller names no ref at all (a GitLab
include:with noref:). It resolves against the default branch, so register that.
After the right reference is registered, scan the shared repository once so the reference has
something behind it, or the result moves to REF_NOT_ACTUALIZED instead.
How much is assessed
One scan resolves at most 256 distinct template references and 64 callsites per chain. Past
either bound the affected callsites are reported as ASSESSMENT_LIMIT_EXCEEDED rather than guessed
at, and sampled evidence never produces a Verified or an Absent: an unassessed callsite stays
unresolved instead of being credited or blamed.
Unlike the discovery bound described under Limits worth knowing, this one is visible on the finding.
Matrix jobs
When a calling job runs a build matrix, the answer for that job is existential: if the control is reached in at least one matrix combination, the job is credited. A job where SAST runs on one of four combinations reads the same as one where it runs on all four.
That is deliberate for now, and worth knowing before you read a Verified on a matrixed job as
"everywhere". If the matrix itself is generated at runtime, the callsite reports MATRIX_DYNAMIC
instead.
Turning it on
Workflow scanning is switched on for your tenant by KvantumCI. There is no setting for it in the product, and nothing in this page is something you enable yourself. Contact support if you want it.
What to expect once it is on, in this order:
- Outcomes appear on findings. Calls into your shared workflows are followed and the result is shown on the finding, with the evidence behind it. Your scores do not change. This is the state you can review against your own pipelines.
- Later, scores begin to reflect it. We enable this separately, and tell you before we do. Scores generally move up, because controls that were reported missing get credited once the shared workflow they live in is read.
Nothing about your repositories changes at either step. What changes is how much of your existing pipeline KvantumCI can see.
Limits worth knowing
A shared workflow is only as current as its last scan. Callers are answered from what the shared repository's own most recent scan recorded. If it changes, re-scan it, or callers keep being credited against the previous version.
A caller's scan never reads the shared repository. It reads what that repository's own scan stored. That is why step 3 above is not optional, and it is also why the caller's credentials are irrelevant: a repository is only ever read by a scan of itself, using the integration you configured for it. A shared repository you have not onboarded is never fetched on a caller's behalf.
Discovery reads at most 64 pipeline files per repository, and does not tell you when it stops. On GitLab and Azure Pipelines, a repository holding more than 64 candidate files has the remainder skipped. The scan records a warning in its log, but nothing surfaces on the finding, the evidence or the scan health. A partial read is currently indistinguishable from a complete one in the product.
The practical consequence: if a very large shared repository shows a control as Absent, confirm the file defining it is within the first 64 discovered before treating the result as authoritative. Splitting a large shared repository into focused ones avoids the limit entirely. We are tracking making the truncation visible; until then, treat this section as the only place it is stated.
Azure has a second bound: template insertion is followed four levels deep within a single repository, separately from the cross-repository depth setting above.
What KvantumCI stores from a shared repository
A scan records what a workflow does (the file path, commit SHA, the control found and the conditions guarding it) and no file content or source code; callers are answered from those records rather than from your repository. These records are kept for 90 days from the scan that wrote them, so a shared repository left unscanned for longer stops answering its callers until it is scanned again.
For data handling generally, see the Privacy Policy.
Related
- Projects: adding repositories and the template repository flag
- Verification Run: running and reviewing a scan
- Findings: reading evidence on a finding
- Rule Engine: how controls are evaluated