Fact Connector
Fact Connector policies let you enrich attestations with data from external backends at publish time. When a CI pipeline publishes an attestation, annotations on the request match against Fact Connector policies and trigger HTTP calls to the configured backends. Each backend response is stored verbatim as a predicate in the attestation.
You choose the predicateType URI that labels each connector response.
Provenance Governor doesn’t constrain that URI beyond blocking reserved types.
How Annotations Route to Fact Connectors
You attach annotations to a publish request to control which Fact Connectors fire and what data they fetch.
Provenance Governor merges your annotations with injected request.package. and request.principal. variables, then matches each connector’s matchAnnotations patterns against the combined map.
A connector fires only when all its patterns match, and matching connectors call their backends concurrently and store each response as its own predicate.
Annotations persist on the subject of every attestation the publish request produces, including Develocity-based attestations, whether or not a Fact Connector matches. Annotations serve as custom metadata on those attestations beyond their routing role, so you can attach them even when no Fact Connector policy is configured.
Publish Endpoint Paths
Send the publish request to one of these paths:
POST /packages/\{type}/\{namespace}/\{name}/\{version}/attestations
POST /packages/\{type}/\{name}/\{version}/attestations
Set annotations with a JSON annotations object or with form bracket notation (annotations[key]=value).
JSON Request Body
The publish endpoint accepts application/json.
The following request carries annotations that route to matching Fact Connector policies:
curl -X POST https://provenance-governor.example.com/packages/maven/com.acme/acme-app/1.0.0/attestations \
-H "Content-Type: application/json" \
-d '{
"sha256": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"repositoryUrl": "artifactory.acme.example.com/maven-releases",
"annotations": {
"sonarqube.project.key": "acme-api",
"github.pull.request": "42",
"teamcity.build.id": "12345",
"vcs.repository": "https://github.com/acme/acme-app",
"vcs.branch": "main",
"vcs.commit": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}
}'
Form Data
The same request as application/x-www-form-urlencoded:
curl -X POST https://provenance-governor.example.com/packages/maven/com.acme/acme-app/1.0.0/attestations \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "sha256=abc123def456abc123def456abc123def456abc123def456abc123def456abc1" \
--data-urlencode "repositoryUrl=artifactory.acme.example.com/maven-releases" \
--data-urlencode "annotations[sonarqube.project.key]=acme-api" \
--data-urlencode "annotations[github.pull.request]=42" \
--data-urlencode "annotations[teamcity.build.id]=12345" \
--data-urlencode "annotations[vcs.repository]=https://github.com/acme/acme-app" \
--data-urlencode "annotations[vcs.branch]=main" \
--data-urlencode "annotations[vcs.commit]=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
This publish request carries six annotations.
Provenance Governor evaluates each Fact Connector’s matchAnnotations against these annotations plus the injected request.package.* variables.
A connector that matches one annotation key fires independently of a connector that matches another.
Each matching connector stores its backend response as a separate predicate in the attestation.
Annotated Reference
The following example shows every spec field with callout annotations explaining each value.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: teamcity-build-info (1)
spec:
predicateType: https://acme.example.com/teamcity/build/v1 (2)
method: GET (3)
uriTemplates: (4)
- "https://teamcity.acme.example.com/app/rest/builds/id:{teamcity.build.id}"
matchAnnotations: (5)
- key: "teamcity.build.id"
- key: "ci.provider"
value: "teamcity" (6)
credentialRef: teamcity (7)
responseTimeout: 30s (8)
maxResponseSize: 1MB (9)
maxConcurrency: 4 (10)
| 1 | Policy name. Appears in startup logs and error messages. |
| 2 | Predicate type URI stored in the in-toto Statement. Must not use reserved Provenance Governor or SLSA types. See Attestations for the list of reserved predicate type URIs. Choose a URI that identifies the backend and response version. |
| 3 | HTTP method. GET sends no request body. Query parameters carry variable values through URI template expansion. |
| 4 | URI templates using {variable} (RFC 6570 Level 1). Each annotation key and request.* variable is available for expansion. |
| 5 | Annotation patterns evaluated with AND semantics: both patterns must match for this connector to fire. |
| 6 | Exact value match. Use ** to match any value including empty. |
| 7 | References a named credential in application configuration. See Credential Configuration. |
| 8 | Maximum backend response wait time. ISO 8601 duration (PT30S) or shorthand (30s). Maximum: 5 minutes. |
| 9 | Maximum response body size. IEC binary notation where KB = 1024 bytes and MB = 1048576 bytes. Maximum: 10 MB. |
| 10 | Maximum concurrent HTTP calls for this Fact Connector across its URI templates. Default: 4. |
Available Request Variables
The following variables are available in URI templates ({variable}), body templates (${variable}), and form templates (${variable}).
| Variable | Description |
|---|---|
|
Package type: |
|
Package namespace. Present only when the package defines a namespace. |
|
Package name. |
|
Package version. For OCI packages, this is the digest |
|
Algorithm-prefixed digest, always |
|
Container image tag. Present only for OCI packages. |
|
Comma-separated list of repository URLs. |
|
Authenticated user’s name. |
|
Individual token claims from the authenticated principal. Only scalar values (String, Number, Boolean) are available. Non-scalar claims (lists, nested objects) are excluded. |
Annotation keys |
Every key from the publish request |
When a template references a variable that isn’t present in the request, Provenance Governor returns HTTP 400 (Bad Request) to the publisher.
Annotation keys must not use the reserved request. prefix (case-insensitive).
An annotation key matching this prefix returns HTTP 400 (Bad Request) to the publisher.
Workload Identity Claims
CI systems that authenticate with OpenID Connect provide workload identity claims as request.principal.claims.* variables.
GitHub Actions tokens include claims such as repository_owner, job_workflow_ref, runner_environment, and repository.
These claims identify the workflow, repository, and execution environment that triggered the publish request.
The Generic Connector example below demonstrates using workload identity claims in body and form templates.
Spec Fields
| Field | Type | Default | Description |
|---|---|---|---|
|
list of string |
(required) |
RFC 6570 Level 1 URI templates expanded with annotation and package variables.
Each entry must start with |
|
string |
(required) |
URI identifying the in-toto predicate type for the connector response. Must not use reserved Provenance Governor or SLSA types. See Attestations for the list of reserved predicate type URIs. Choose a URI that identifies the backend and response version. |
|
string |
|
HTTP method: |
|
string |
(optional) |
Name of a credential defined in |
|
list of AnnotationPattern |
(required) |
Patterns matched against publish-request variables. All patterns must match for the connector to fire (AND semantics). See Annotation Pattern Syntax. |
|
duration |
|
Maximum time to wait for the backend response.
Accepts shorthand notation ( |
|
data size |
|
Maximum response body size.
Accepts data size notation ( |
|
map of string to string |
(optional) |
Form fields sent as |
|
string |
(optional) |
Request body template with |
|
string |
|
Media type for |
|
int |
|
Maximum concurrent HTTP calls for this Fact Connector across its URI templates. Default: 4. Must be >= 1. |
Annotation Pattern Syntax
matchAnnotations entries have two fields:
key-
Glob pattern matched against variable keys (annotation keys and injected
request.*keys). value-
Glob pattern matched against the corresponding value. When omitted, the connector matches any value for a matched key.
Matching is case-insensitive.
Patterns split on / into segments.
A single * matches one non-empty path segment only.
It doesn’t match an empty value, and it doesn’t match across /.
To match all values including empty and slash-containing values, write \**, not \*.
A null or omitted value means "key present, any value".
The following patterns show how prefix, suffix, path-segment, and catch-all wildcards behave:
| Pattern | Matches | Doesn’t Match |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
any key, including |
(matches every key) |
A simple prefix such as acme-build- matches a key that starts with acme-build- followed by one path segment.
A simple suffix such as -release matches a value that ends with -release.
In a path pattern, * matches one segment and matches any remaining segments including an empty one.
A lone matches any key, including keys that contain slashes.
URI Template Expansion
URI templates (uriTemplates) follow RFC 6570 Level 1.
Use {variable} placeholders:
https://api.example.com/status?project=\{sonarqube.project.key}&pkg=\{request.package.name}
When a URI template contains a variable that’s absent or invalid, Provenance Governor returns HTTP 400 (Bad Request) to the publisher.
See Available Request Variables for the complete list of available variables.
Body and Form Template Expansion
Body templates (bodyTemplate) and form template values (formTemplate values) use placeholder syntax.
Use ${variable} placeholders:
{
"name": "$\{request.package.name}",
"version": "$\{request.package.version}"
}
See Available Request Variables for the complete list of available variables.
Accept Header
The connector sends Accept: application/json on every request.
Backends must return JSON.
The in-toto specification requires predicates to be JSON.
Raw JSON Fidelity
The connector stores the backend response as raw JSON without parsing or re-serializing. Number precision, key ordering, and null handling are preserved exactly as the backend returned them.
Failure Behavior
-
Unresolved or invalid template variables return HTTP 400 (Bad Request) to the publisher.
-
Backend HTTP call failures after retry exhaustion propagate the original error to the caller.
-
The connector doesn’t silently drop errors.
Startup Logging
Each connector logs its policy name, credential ref (when configured), and credential header key names (never values) at INFO level during startup.
Credential Configuration
Connector credentials are configured through Provenance Governor’s Application Configuration using the properties or secrets directories.
Configure named credentials with static HTTP headers for Fact Connector authentication, keeping secrets separate from GitOps-reviewed policy YAML.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
|
map of string to string |
(none) |
Static HTTP headers added to every connector request for the named credential.
The |
Validation Rules
-
Header values must not be wrapped in double quotes. Application property binding preserves literal quotes. A value of
"Bearer TOKEN"sends the quotes on the wire. -
Leading and trailing whitespace is stripped from keys and values.
-
Exactly one strategy field must be active per credential. The
oauth2strategy is reserved for a future release.
Configuration Example
connector:
credentials:
sonarcloud:
headers:
Authorization: Basic SONARCLOUD_TOKEN
servicenow:
headers:
Authorization: Basic SERVICENOW_CREDENTIALS
teamcity:
headers:
Authorization: Bearer TEAMCITY_TOKEN
SONARCLOUD_TOKEN, SERVICENOW_CREDENTIALS, and TEAMCITY_TOKEN are placeholders.
Replace each with the actual credential value from your secrets manager before deployment.
Examples
The predicate field in each in-toto Statement below contains the raw JSON response from the backend service, preserved without modification.
The actual fields and values depend on the backend’s API, so they differ from one example to the next.
TeamCity Build Info
A CI pipeline publishes an attestation after completing a build.
The teamcity.build.id annotation identifies the build that produced the artifact, and the vcs.repository and vcs.branch annotations record the source context.
This connector retrieves the full build record from TeamCity and stores it as an attestation predicate.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: teamcity-build-info
spec:
predicateType: https://acme.example.com/teamcity/build/v1
method: GET
uriTemplates:
- "https://teamcity.acme.example.com/app/rest/builds/id:{teamcity.build.id}"
matchAnnotations:
- key: "teamcity.build.id"
- key: "vcs.repository"
- key: "vcs.branch"
credentialRef: teamcity
responseTimeout: 30s
The teamcity credential uses Bearer authentication:
connector:
credentials:
teamcity:
headers:
Authorization: Bearer TEAMCITY_TOKEN
TEAMCITY_TOKEN is a personal access token generated in the TeamCity UI under user profile > Access Tokens.
See the TeamCity REST API documentation for the full response schema.
The resulting in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "pkg:maven/com.acme/acme-app@1.0.0",
"digest": {
"sha256": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
},
"annotations": {
"teamcity.build.id": "12345",
"vcs.repository": "https://github.com/acme/acme-app",
"vcs.branch": "main",
"vcs.commit": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}
}
],
"predicateType": "https://acme.example.com/teamcity/build/v1",
"predicate": {
"id": 12345,
"buildTypeId": "AcmeApp_Build",
"number": "42",
"status": "SUCCESS",
"state": "finished",
"branchName": "main",
"href": "/app/rest/builds/id:12345",
"webUrl": "https://teamcity.acme.example.com/viewLog.html?buildId=12345&buildTypeId=AcmeApp_Build",
"statusText": "Success",
"buildType": {
"id": "AcmeApp_Build",
"name": "Acme App Build",
"projectName": "Acme",
"projectId": "Acme"
},
"startDate": "20240615T103005+0000",
"finishDate": "20240615T103045+0000",
"revisions": {
"count": 1,
"revision": [
{
"version": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
"vcsBranchName": "refs/heads/main"
}
]
}
}
}
All annotations provided on the publish request appear in the subject’s annotations field.
The predicate body contains the raw JSON response from the backend, preserved without modification.
SonarCloud Quality Gate
A CI pipeline runs a SonarQube analysis on a pull request. The publish request carries the project key and pull request number as annotations. This connector calls the SonarCloud quality gate API and stores the pass/fail result with metric conditions as an attestation predicate.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: sonarcloud-quality-gate
spec:
predicateType: https://acme.example.com/sonarcloud/quality-gate/v1
method: GET
uriTemplates:
- "https://sonarcloud.io/api/qualitygates/project_status?projectKey={sonarqube.project.key}&pullRequest={github.pull.request}"
matchAnnotations:
- key: "sonarqube.project.key"
- key: "github.pull.request"
credentialRef: sonarcloud
responseTimeout: 15s
The sonarcloud credential uses Basic Auth with a user token:
connector:
credentials:
sonarcloud:
headers:
Authorization: Basic SONARCLOUD_TOKEN
SONARCLOUD_TOKEN is a base64-encoded <user-token>: string (token as username, empty password) per the SonarCloud authentication model.
The quality gate response is scoped to the pull request analysis.
SonarCloud doesn’t include the Git commit SHA in this response.
The pullRequest parameter ties the result to the specific code change.
See the SonarCloud API documentation for the full response schema.
The resulting in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "pkg:maven/com.acme/acme-app@1.0.0",
"digest": {
"sha256": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
},
"annotations": {
"sonarqube.project.key": "acme-api",
"github.pull.request": "42"
}
}
],
"predicateType": "https://acme.example.com/sonarcloud/quality-gate/v1",
"predicate": {
"projectStatus": {
"status": "OK",
"conditions": [
{
"status": "OK",
"metricKey": "new_reliability_rating",
"comparator": "GT",
"periodIndex": 1,
"errorThreshold": "1",
"actualValue": "1"
},
{
"status": "OK",
"metricKey": "new_security_rating",
"comparator": "GT",
"periodIndex": 1,
"errorThreshold": "1",
"actualValue": "1"
}
],
"periods": [
{
"index": 1,
"mode": "previous_version",
"date": "2024-06-15T10:30:00+0000"
}
],
"ignoredConditions": false
}
}
}
ServiceNow Change Request
A deployment pipeline publishes an attestation for a production-bound package.
The deploy.environment annotation signals a production deployment.
This connector creates a change request in ServiceNow, and the created record is stored as an attestation predicate.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: servicenow-change-request
spec:
predicateType: https://acme.example.com/servicenow/change/v1
method: POST
uriTemplates:
- "https://acme.service-now.com/api/now/table/change_request"
bodyTemplate: |
{
"short_description": "Deploy ${request.package.name}:${request.package.version}",
"description": "Automated change request for package ${request.package.name} version ${request.package.version} with checksum ${request.package.checksum}",
"category": "Software",
"type": "standard"
}
bodyContentType: application/json
matchAnnotations:
- key: "deploy.environment"
value: "production"
credentialRef: servicenow
responseTimeout: 15s
maxResponseSize: 512KB
maxConcurrency: 2
See the ServiceNow Table API documentation for available request fields and the response schema.
The resulting in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "pkg:maven/com.acme/acme-app@1.0.0",
"digest": {
"sha256": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
},
"annotations": {
"deploy.environment": "production"
}
}
],
"predicateType": "https://acme.example.com/servicenow/change/v1",
"predicate": {
"result": {
"sys_id": "46ca9f72a9fe198100572e90c7cbce18",
"number": "CHG0001234",
"type": "standard",
"state": "1",
"short_description": "Deploy acme-app:1.0.0",
"description": "Automated change request for package acme-app version 1.0.0 with checksum sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"sys_created_on": "2024-06-15 10:30:00",
"sys_updated_on": "2024-06-15 10:30:00"
}
}
}
Generic Connector
The Fact Connector model supports any backend that accepts HTTP requests and returns JSON. Common use cases include:
-
Change management systems (ServiceNow, Jira Service Management)
-
Security scanning platforms (Snyk, Checkmarx)
-
Compliance and audit logging
-
Internal build metadata APIs
-
Artifact promotion and approval workflows
The following examples show the full range of template variables and expansion styles.
Body templates send the expanded content as the HTTP request body with the media type set by bodyContentType.
Form templates send each field as an application/x-www-form-urlencoded key-value pair.
Both use ${variable} placeholder expansion.
A Fact Connector policy uses one or the other, never both, so the two templates are mutually exclusive per policy.
Both examples match on key: "".
This connector fires on every publish request.
The pattern matches all variables in the request map, including injected request.package. and request.principal. variables.
Body Template Example
Use a body template when the backend expects a structured JSON payload, such as an API that consumes JSON. A JSON body template captures principal claims and sends them as the request body.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: publish-notify-json
spec:
predicateType: https://acme.example.com/webhook/notify/v1
method: POST
uriTemplates:
- "https://hooks.acme.example.com/publish-notify"
bodyTemplate: |
{
"package": "${request.package.name}",
"version": "${request.package.version}",
"checksum": "${request.package.checksum}",
"type": "${request.package.type}",
"repository": "${request.package.repositoryUrl}",
"actor": "${request.principal.name}",
"repository_owner": "${request.principal.claims.repository_owner}",
"job_workflow_ref": "${request.principal.claims.job_workflow_ref}",
"runner_environment": "${request.principal.claims.runner_environment}"
}
bodyContentType: application/json
matchAnnotations:
- key: "**"
credentialRef: internal-webhook
Form Template Example
Use a form template for simple key-value backends, such as webhook receivers.
A form template covers simpler backends that accept application/x-www-form-urlencoded fields.
apiVersion: policy.gradle.com/v1
kind: FactConnector
metadata:
name: publish-notify-form
spec:
predicateType: https://acme.example.com/webhook/notify/v1
method: POST
uriTemplates:
- "https://hooks.acme.example.com/publish-notify"
formTemplate:
package: "${request.package.name}"
version: "${request.package.version}"
checksum: "${request.package.checksum}"
actor: "${request.principal.name}"
repository_owner: "${request.principal.claims.repository_owner}"
job_workflow_ref: "${request.principal.claims.job_workflow_ref}"
runner_environment: "${request.principal.claims.runner_environment}"
matchAnnotations:
- key: "**"
credentialRef: internal-webhook
To limit a connector to specific package types, match on request.package.type:
matchAnnotations:
- key: "request.package.type"
value: "oci"
This connector fires only for OCI package publish requests.
The resulting in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "pkg:maven/com.acme/acme-app@1.0.0",
"digest": {
"sha256": "abc123def456abc123def456abc123def456abc123def456abc123def456abc1"
},
"annotations": {
"deploy.env": "staging",
"build.id": "999"
}
}
],
"predicateType": "https://acme.example.com/webhook/notify/v1",
"predicate": {
"status": "accepted",
"id": "evt-abc123",
"timestamp": "2024-06-15T10:30:00Z"
}
}