diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index a23346ade..4198c0f84 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -252,7 +252,7 @@ Options --annotation strings additional annotation in the format of key=value --attestation-id string Unique identifier of the in-progress attestation -h, --help help for add ---kind string kind of the material to be recorded: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] +--kind string kind of the material to be recorded: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] --name string name of the material as shown in the contract --no-strict-validation skip strict schema validation for structured materials (SBOM_CYCLONEDX_JSON, OPENAPI_SPEC, ASYNCAPI_SPEC) --registry-password string registry password, ($CHAINLOOP_REGISTRY_PASSWORD) @@ -3025,7 +3025,7 @@ Options --annotation strings Key-value pairs of material annotations (key=value) -h, --help help for eval --input stringArray Key-value pairs of policy inputs (key=value) ---kind string Kind of the material: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] +--kind string Kind of the material: ["ARTIFACT" "ASYNCAPI_SPEC" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "GRAPHQL_SPEC" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENAPI_SPEC" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "SYSINTERNALS_ACCESSCHK" "SYSINTERNALS_SIGCHECK" "TWISTCLI_SCAN_JSON" "YELP_DETECT_SECRETS_BASELINE" "ZAP_DAST_ZIP"] --material string Path to material or attestation file -p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml") --project string Project name to use as engine context for chainloop.* built-ins diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index e03898df9..e31cac21c 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -285,6 +285,8 @@ export enum CraftingSchema_Material_MaterialType { * https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck */ SYSINTERNALS_SIGCHECK = 34, + /** SYSINTERNALS_ACCESSCHK - Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk */ + SYSINTERNALS_ACCESSCHK = 35, UNRECOGNIZED = -1, } @@ -395,6 +397,9 @@ export function craftingSchema_Material_MaterialTypeFromJSON(object: any): Craft case 34: case "SYSINTERNALS_SIGCHECK": return CraftingSchema_Material_MaterialType.SYSINTERNALS_SIGCHECK; + case 35: + case "SYSINTERNALS_ACCESSCHK": + return CraftingSchema_Material_MaterialType.SYSINTERNALS_ACCESSCHK; case -1: case "UNRECOGNIZED": default: @@ -474,6 +479,8 @@ export function craftingSchema_Material_MaterialTypeToJSON(object: CraftingSchem return "YELP_DETECT_SECRETS_BASELINE"; case CraftingSchema_Material_MaterialType.SYSINTERNALS_SIGCHECK: return "SYSINTERNALS_SIGCHECK"; + case CraftingSchema_Material_MaterialType.SYSINTERNALS_ACCESSCHK: + return "SYSINTERNALS_ACCESSCHK"; case CraftingSchema_Material_MaterialType.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json index f0b068b8f..8b4bb3404 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.jsonschema.json @@ -51,7 +51,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" @@ -138,7 +139,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json index 5ef0f1676..d3afb88d5 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.Attestation.Material.schema.json @@ -51,7 +51,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" @@ -138,7 +139,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json index 769851142..c34c506d9 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.jsonschema.json @@ -148,7 +148,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json index 32c9991ff..5b530f697 100644 --- a/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json +++ b/app/controlplane/api/gen/jsonschema/attestation.v1.PolicyEvaluation.schema.json @@ -148,7 +148,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json index 1f0002a1f..91dcb1255 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.jsonschema.json @@ -68,7 +68,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json index a6aed6d6f..bd08a002a 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.CraftingSchema.Material.schema.json @@ -68,7 +68,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json index 997d140ad..5274ebca5 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.jsonschema.json @@ -56,7 +56,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json index a4136862a..1ed300a2b 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicyGroup.Material.schema.json @@ -56,7 +56,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json index 75aa9fbad..81fe80e71 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.jsonschema.json @@ -70,7 +70,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json index 190ce82ac..b3452c1d5 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpec.schema.json @@ -70,7 +70,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json index d2d76eff4..69960c6e3 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -91,7 +91,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json index 27346d5ed..aa98bd142 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -91,7 +91,8 @@ "ASYNCAPI_SPEC", "GRAPHQL_SPEC", "YELP_DETECT_SECRETS_BASELINE", - "SYSINTERNALS_SIGCHECK" + "SYSINTERNALS_SIGCHECK", + "SYSINTERNALS_ACCESSCHK" ], "title": "Material Type", "type": "string" diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 19b2de6e4..c52129985 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -227,6 +227,8 @@ const ( // Sysinternals sigcheck output in CSV format // https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck CraftingSchema_Material_SYSINTERNALS_SIGCHECK CraftingSchema_Material_MaterialType = 34 + // Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk + CraftingSchema_Material_SYSINTERNALS_ACCESSCHK CraftingSchema_Material_MaterialType = 35 ) // Enum value maps for CraftingSchema_Material_MaterialType. @@ -267,6 +269,7 @@ var ( 32: "GRAPHQL_SPEC", 33: "YELP_DETECT_SECRETS_BASELINE", 34: "SYSINTERNALS_SIGCHECK", + 35: "SYSINTERNALS_ACCESSCHK", } CraftingSchema_Material_MaterialType_value = map[string]int32{ "MATERIAL_TYPE_UNSPECIFIED": 0, @@ -304,6 +307,7 @@ var ( "GRAPHQL_SPEC": 32, "YELP_DETECT_SECRETS_BASELINE": 33, "SYSINTERNALS_SIGCHECK": 34, + "SYSINTERNALS_ACCESSCHK": 35, } ) @@ -1965,7 +1969,7 @@ var File_workflowcontract_v1_crafting_schema_proto protoreflect.FileDescriptor const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\n" + - ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\xf5\x0f\n" + + ")workflowcontract/v1/crafting_schema.proto\x12\x13workflowcontract.v1\x1a\x1bbuf/validate/validate.proto\"\x91\x10\n" + "\x0eCraftingSchema\x122\n" + "\x0eschema_version\x18\x01 \x01(\tB\v\xbaH\x06r\x04\n" + "\x02v1\x18\x01R\rschemaVersion\x12N\n" + @@ -1988,7 +1992,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x0fDAGGER_PIPELINE\x10\x06\x12\x15\n" + "\x11TEAMCITY_PIPELINE\x10\a\x12\x13\n" + "\x0fTEKTON_PIPELINE\x10\b\x12\x15\n" + - "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xc0\t\n" + + "\x11CHAINLOOP_SANDBOX\x10\t:\x02\x18\x01\x1a\xdc\t\n" + "\bMaterial\x12[\n" + "\x04type\x18\x01 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\f\xbaH\a\x82\x01\x04\x10\x01 \x00\x18\x01R\x04type\x12\x99\x01\n" + "\x04name\x18\x02 \x01(\tB\x84\x01\xbaH\x7f\xba\x01|\n" + @@ -1997,7 +2001,7 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x06output\x18\x04 \x01(\bB\x02\x18\x01R\x06output\x12E\n" + "\vannotations\x18\x05 \x03(\v2\x1f.workflowcontract.v1.AnnotationB\x02\x18\x01R\vannotations\x12\x1f\n" + "\vskip_upload\x18\x06 \x01(\bR\n" + - "skipUpload\"\x92\x06\n" + + "skipUpload\"\xae\x06\n" + "\fMaterialType\x12\x1d\n" + "\x19MATERIAL_TYPE_UNSPECIFIED\x10\x00\x12\n" + "\n" + @@ -2037,7 +2041,8 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\rASYNCAPI_SPEC\x10\x1f\x12\x10\n" + "\fGRAPHQL_SPEC\x10 \x12 \n" + "\x1cYELP_DETECT_SECRETS_BASELINE\x10!\x12\x19\n" + - "\x15SYSINTERNALS_SIGCHECK\x10\":\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + + "\x15SYSINTERNALS_SIGCHECK\x10\"\x12\x1a\n" + + "\x16SYSINTERNALS_ACCESSCHK\x10#:\x02\x18\x01:\x02\x18\x01\"\xfb\x01\n" + "\x10CraftingSchemaV2\x128\n" + "\vapi_version\x18\x01 \x01(\tB\x17\xbaH\x14r\x12\n" + "\x10chainloop.dev/v1R\n" + diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index 1374e7807..efbc50c80 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -168,6 +168,8 @@ message CraftingSchema { // Sysinternals sigcheck output in CSV format // https://learn.microsoft.com/en-us/sysinternals/downloads/sigcheck SYSINTERNALS_SIGCHECK = 34; + // Sysinternals AccessChk text output https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk + SYSINTERNALS_ACCESSCHK = 35; } } } diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go index 8a2f2e5c2..fe4a9a281 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state.go @@ -25,6 +25,7 @@ import ( "strings" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/accesschk" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/attestation" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/jacoco" materialsjunit "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/junit" @@ -153,6 +154,18 @@ func (m *Attestation_Material) GetEvaluableContent(value string) ([]byte, error) if err != nil { return nil, fmt.Errorf("failed to marshal sigcheck report: %w", err) } + case v1.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK: + // AccessChk emits plain text; project it to JSON so the policy engine, + // which only consumes JSON, can evaluate it. The raw text is preserved + // in the projection's "raw" field for string-matching fallbacks. + report, perr := accesschk.Parse(rawMaterial) + if perr != nil { + return nil, fmt.Errorf("invalid accesschk material: %w", perr) + } + rawMaterial, err = json.Marshal(report) + if err != nil { + return nil, fmt.Errorf("failed to marshal accesschk material: %w", err) + } } // if raw material is empty (container images, for example), let's create an empty json diff --git a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go index b4d88cbea..537feef97 100644 --- a/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go +++ b/pkg/attestation/crafter/api/attestation/v1/crafting_state_test.go @@ -212,6 +212,20 @@ func TestGetEvaluableContentWithMetadata(t *testing.T) { filename: "testdata/sigcheck-report.csv", testField: "elements", }, + { + name: "accesschk text material projected to json", + material: &Attestation_Material{ + MaterialType: schemaapi.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK, + M: &Attestation_Material_Artifact_{ + Artifact: &Attestation_Material_Artifact{ + Name: "name", Digest: "sha256:deadbeef", IsSubject: true, + Content: []byte("c:\\windows\\system32\\notepad.exe\n RW BUILTIN\\Administrators\n"), + }, + }, + InlineCas: true, + }, + testField: "objects", + }, } for _, tc := range cases { diff --git a/pkg/attestation/crafter/materials/accesschk.go b/pkg/attestation/crafter/materials/accesschk.go new file mode 100644 index 000000000..934f76b40 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk.go @@ -0,0 +1,83 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials + +import ( + "context" + "fmt" + "os" + + schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/accesschk" + "github.com/chainloop-dev/chainloop/pkg/casclient" + "github.com/rs/zerolog" +) + +// AccessChkCrafter stores the text output of the Sysinternals AccessChk tool as +// supply-chain evidence. The raw text is stored as-is; the text-to-JSON +// projection used by the policy engine happens later, at evaluation time. +type AccessChkCrafter struct { + *crafterCommon + backend *casclient.CASBackend +} + +func NewAccessChkCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*AccessChkCrafter, error) { + if schema.Type != schemaapi.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK { + return nil, fmt.Errorf("material type is not an accesschk output") + } + craftCommon := &crafterCommon{logger: l, input: schema} + return &AccessChkCrafter{backend: backend, crafterCommon: craftCommon}, nil +} + +func (i *AccessChkCrafter) Craft(ctx context.Context, filePath string) (*api.Attestation_Material, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("can't open the file: %w", err) + } + + // Soft fingerprint: AccessChk emits free-form text, so we only require that + // the input is valid text that resembles AccessChk output (a banner, at + // least one access entry, or an SDDL/descriptor marker). The raw text is + // stored unchanged; it is projected to JSON later for policy evaluation. + report, err := accesschk.Parse(data) + if err != nil { + return nil, fmt.Errorf("invalid accesschk output: %w", ErrInvalidMaterialType) + } + + if !report.LooksLikeAccessChk() { + return nil, fmt.Errorf("input does not look like accesschk output: %w", ErrInvalidMaterialType) + } + + m, err := uploadAndCraft(ctx, i.input, i.backend, filePath, i.logger) + if err != nil { + return nil, err + } + + i.injectAnnotations(m, report) + + return m, nil +} + +func (i *AccessChkCrafter) injectAnnotations(m *api.Attestation_Material, report *accesschk.Report) { + if m.Annotations == nil { + m.Annotations = make(map[string]string) + } + m.Annotations[AnnotationToolNameKey] = report.Tool.Name + if report.Tool.Version != "" { + m.Annotations[AnnotationToolVersionKey] = report.Tool.Version + } +} diff --git a/pkg/attestation/crafter/materials/accesschk/accesschk.go b/pkg/attestation/crafter/materials/accesschk/accesschk.go new file mode 100644 index 000000000..e4b928b07 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/accesschk.go @@ -0,0 +1,283 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package accesschk parses the text output of the Sysinternals AccessChk tool +// (https://learn.microsoft.com/en-us/sysinternals/downloads/accesschk) into a +// structured representation. AccessChk has no machine-readable output mode, so +// the parser is intentionally tolerant: anything it cannot recognize is +// preserved verbatim and the full original text is always retained in Raw, so a +// policy can fall back to string matching regardless of the output mode used. +package accesschk + +import ( + "fmt" + "regexp" + "strings" + "unicode/utf8" +) + +// ToolName is the canonical tool name recorded for AccessChk materials. +const ToolName = "AccessChk" + +// versionRe extracts the AccessChk version from its banner, e.g. "Accesschk v6.15". +var versionRe = regexp.MustCompile(`(?i)accesschk v([0-9][0-9.]*)`) + +// accessEntryRe matches a per-principal access line such as " RW BUILTIN\Administrators". +// The access token (R, W or RW) must be followed by whitespace, which prevents +// right names like "WRITE_DAC" or "READ_CONTROL" from being mistaken for entries. +var accessEntryRe = regexp.MustCompile(`^(RW|R|W)\s+(\S.*)$`) + +// aceRe matches a numbered ACE line emitted under -l, in both the DACL form +// "[0] ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\SYSTEM" and the SACL form +// "[0] : Everyone" (where the ACE type is empty). +var aceRe = regexp.MustCompile(`^\[(\d+)\]\s*(.*?):\s*(.*)$`) + +// Tool holds the tool identity parsed from the AccessChk banner. +type Tool struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` +} + +// AccessEntry is a single principal and the access it was granted on an object, +// as reported by the compact default (R/W) output mode. +type AccessEntry struct { + Access string `json:"access"` + Principal string `json:"principal"` + Rights []string `json:"rights"` +} + +// ACE is a single access control entry from a security descriptor reported by +// the -l output mode (DACL or SACL). +type ACE struct { + Index int `json:"index"` + AceType string `json:"ace_type,omitempty"` + Principal string `json:"principal"` + AceFlags []string `json:"ace_flags"` + Rights []string `json:"rights"` +} + +// Object is a single securable object reported by AccessChk. +// +// AccessEntries is populated by the compact default mode; DescriptorFlags, +// Owner, DACL and SACL are populated by the -l (full security descriptor) mode. +// RawLines always holds every indented line verbatim regardless of mode. +type Object struct { + Name string `json:"name"` + DescriptorFlags []string `json:"descriptor_flags,omitempty"` + Owner string `json:"owner,omitempty"` + DACL []ACE `json:"dacl,omitempty"` + SACL []ACE `json:"sacl,omitempty"` + AccessEntries []AccessEntry `json:"access_entries"` + RawLines []string `json:"raw_lines"` +} + +// Report is the structured projection of an AccessChk run. +type Report struct { + Tool Tool `json:"tool"` + Objects []Object `json:"objects"` + Raw string `json:"raw"` +} + +// Parse converts AccessChk text output into a Report. It only returns an error +// when the input is not valid UTF-8 text; well-formed text always parses, with +// any unrecognized content preserved in the per-object RawLines and the +// top-level Raw field. +func Parse(data []byte) (*Report, error) { + if !utf8.Valid(data) { + return nil, fmt.Errorf("input is not valid UTF-8 text") + } + + raw := string(data) + report := &Report{ + Tool: Tool{Name: ToolName}, + Objects: []Object{}, + Raw: raw, + } + + if m := versionRe.FindStringSubmatch(raw); m != nil { + report.Tool.Version = m[1] + } + + normalized := strings.ReplaceAll(raw, "\r\n", "\n") + var current *Object + var entryIndent int + + // State for the -l (full security descriptor) mode. + const ( + sectNone = iota + sectDescriptorFlags + sectDACL + sectSACL + ) + section := sectNone + var currentACE *ACE + + for _, line := range strings.Split(normalized, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" || isBannerLine(trimmed) { + continue + } + + indent := len(line) - len(strings.TrimLeft(line, " \t")) + + // A line at column zero starts a new object. + if indent == 0 { + report.Objects = append(report.Objects, Object{ + Name: trimmed, + AccessEntries: []AccessEntry{}, + RawLines: []string{}, + }) + current = &report.Objects[len(report.Objects)-1] + entryIndent = -1 + section = sectNone + currentACE = nil + continue + } + + // Indented content before any object header is dropped. + if current == nil { + continue + } + + current.RawLines = append(current.RawLines, line) + + // Compact default (R/W) output mode. + if m := accessEntryRe.FindStringSubmatch(trimmed); m != nil { + current.AccessEntries = append(current.AccessEntries, AccessEntry{ + Access: m[1], + Principal: m[2], + Rights: []string{}, + }) + entryIndent = indent + currentACE = nil + continue + } + + // -l (full security descriptor) section headers. + switch { + case strings.HasPrefix(trimmed, "DESCRIPTOR FLAGS"): + section = sectDescriptorFlags + currentACE = nil + continue + case strings.HasPrefix(trimmed, "OWNER:"): + current.Owner = strings.TrimSpace(strings.TrimPrefix(trimmed, "OWNER:")) + currentACE = nil + continue + case strings.HasPrefix(trimmed, "DACL"): + section = sectDACL + currentACE = nil + continue + case strings.HasPrefix(trimmed, "SACL"): + section = sectSACL + currentACE = nil + continue + } + + // -l numbered ACE lines (DACL by default, SACL once inside a SACL block). + if m := aceRe.FindStringSubmatch(trimmed); m != nil { + ace := ACE{ + Index: atoi(m[1]), + AceType: strings.TrimSpace(m[2]), + Principal: strings.TrimSpace(m[3]), + AceFlags: []string{}, + Rights: []string{}, + } + if section == sectSACL { + current.SACL = append(current.SACL, ace) + currentACE = ¤t.SACL[len(current.SACL)-1] + } else { + section = sectDACL + current.DACL = append(current.DACL, ace) + currentACE = ¤t.DACL[len(current.DACL)-1] + } + continue + } + + // Detail lines: bracketed tokens are flags, bare tokens are rights. + isFlag := strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") + token := strings.TrimSuffix(strings.TrimPrefix(trimmed, "["), "]") + + if currentACE != nil { + if isFlag { + currentACE.AceFlags = append(currentACE.AceFlags, token) + } else { + currentACE.Rights = append(currentACE.Rights, trimmed) + } + continue + } + + if section == sectDescriptorFlags { + current.DescriptorFlags = append(current.DescriptorFlags, token) + continue + } + + // A line indented deeper than the compact access entry it follows is a + // specific right (only emitted under -v); attach it to the entry. + if entryIndent >= 0 && indent > entryIndent && len(current.AccessEntries) > 0 { + last := ¤t.AccessEntries[len(current.AccessEntries)-1] + last.Rights = append(last.Rights, trimmed) + } + } + + return report, nil +} + +// atoi parses a non-negative integer, returning 0 on failure. ACE indexes are +// always well-formed in AccessChk output, so this keeps the caller simple. +func atoi(s string) int { + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + return 0 + } + n = n*10 + int(r-'0') + } + return n +} + +// LooksLikeAccessChk reports whether the parsed report resembles genuine +// AccessChk output. It is deliberately lenient: a recognizable banner, at least +// one parsed access entry, or an SDDL/descriptor marker is enough. +func (r *Report) LooksLikeAccessChk() bool { + if r.Tool.Version != "" { + return true + } + for _, o := range r.Objects { + if len(o.AccessEntries) > 0 || len(o.DACL) > 0 || len(o.SACL) > 0 || + o.Owner != "" || len(o.DescriptorFlags) > 0 { + return true + } + } + if strings.Contains(r.Raw, "DESCRIPTOR FLAGS") || strings.Contains(r.Raw, "ACCESS_ALLOWED") { + return true + } + return false +} + +// isBannerLine reports whether a trimmed line belongs to the AccessChk startup +// banner/copyright, which must not be treated as an object or access entry. +func isBannerLine(trimmed string) bool { + lower := strings.ToLower(trimmed) + switch { + case strings.HasPrefix(lower, "accesschk v"): + return true + case strings.HasPrefix(lower, "copyright"): + return true + case strings.Contains(lower, "sysinternals - www.sysinternals.com"): + return true + default: + return false + } +} diff --git a/pkg/attestation/crafter/materials/accesschk/accesschk_test.go b/pkg/attestation/crafter/materials/accesschk/accesschk_test.go new file mode 100644 index 000000000..ca4526b75 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/accesschk_test.go @@ -0,0 +1,186 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package accesschk_test + +import ( + "os" + "testing" + + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials/accesschk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParse_Default(t *testing.T) { + data, err := os.ReadFile("./testdata/default.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + + assert.Equal(t, "AccessChk", report.Tool.Name) + assert.Equal(t, "6.15", report.Tool.Version) + assert.Equal(t, string(data), report.Raw) + assert.True(t, report.LooksLikeAccessChk()) + + require.Len(t, report.Objects, 1) + obj := report.Objects[0] + assert.Equal(t, `c:\windows\system32\notepad.exe`, obj.Name) + + require.Len(t, obj.AccessEntries, 3) + assert.Equal(t, "RW", obj.AccessEntries[0].Access) + assert.Equal(t, `NT AUTHORITY\SYSTEM`, obj.AccessEntries[0].Principal) + assert.Empty(t, obj.AccessEntries[0].Rights) + assert.Equal(t, "RW", obj.AccessEntries[1].Access) + assert.Equal(t, `BUILTIN\Administrators`, obj.AccessEntries[1].Principal) + assert.Equal(t, "R", obj.AccessEntries[2].Access) + assert.Equal(t, `BUILTIN\Users`, obj.AccessEntries[2].Principal) + + assert.Equal(t, []string{ + ` RW NT AUTHORITY\SYSTEM`, + ` RW BUILTIN\Administrators`, + ` R BUILTIN\Users`, + }, obj.RawLines) +} + +func TestParse_Verbose(t *testing.T) { + data, err := os.ReadFile("./testdata/verbose.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + + require.Len(t, report.Objects, 1) + entries := report.Objects[0].AccessEntries + require.Len(t, entries, 2) + + assert.Equal(t, "RW", entries[0].Access) + assert.Equal(t, `NT AUTHORITY\SYSTEM`, entries[0].Principal) + assert.Equal(t, []string{"FILE_ALL_ACCESS"}, entries[0].Rights) + + assert.Equal(t, "R", entries[1].Access) + assert.Equal(t, `BUILTIN\Users`, entries[1].Principal) + assert.Equal(t, []string{"FILE_EXECUTE", "FILE_READ_ATTRIBUTES", "FILE_READ_DATA"}, entries[1].Rights) +} + +func TestParse_Service(t *testing.T) { + data, err := os.ReadFile("./testdata/service.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + + require.Len(t, report.Objects, 1) + assert.Equal(t, "spooler", report.Objects[0].Name) + require.Len(t, report.Objects[0].AccessEntries, 2) + assert.Equal(t, []string{"SERVICE_ALL_ACCESS"}, report.Objects[0].AccessEntries[0].Rights) + assert.True(t, report.LooksLikeAccessChk()) +} + +func TestParse_SDDL(t *testing.T) { + data, err := os.ReadFile("./testdata/sddl.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + + require.Len(t, report.Objects, 1) + obj := report.Objects[0] + assert.Equal(t, `c:\windows\system32\notepad.exe`, obj.Name) + // SDDL/descriptor output is not parsed into structured access entries, + // but it is preserved verbatim in raw_lines for policy string matching. + assert.Empty(t, obj.AccessEntries) + assert.Contains(t, obj.RawLines, " DESCRIPTOR FLAGS:") + assert.Contains(t, obj.RawLines, " OWNER: NT SERVICE\\TrustedInstaller") + assert.True(t, report.LooksLikeAccessChk()) +} + +func TestParse_NoBanner(t *testing.T) { + data, err := os.ReadFile("./testdata/nobanner.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + + assert.Equal(t, "AccessChk", report.Tool.Name) + assert.Empty(t, report.Tool.Version) + require.Len(t, report.Objects, 1) + assert.Equal(t, `c:\windows\system32\notepad.exe`, report.Objects[0].Name) + require.Len(t, report.Objects[0].AccessEntries, 2) + assert.True(t, report.LooksLikeAccessChk()) +} + +func TestParse_DescriptorFormat(t *testing.T) { + data, err := os.ReadFile("./testdata/descriptor.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + assert.True(t, report.LooksLikeAccessChk()) + + require.Len(t, report.Objects, 1) + obj := report.Objects[0] + assert.Equal(t, "ExampleService", obj.Name) + assert.Equal(t, `NT AUTHORITY\SYSTEM`, obj.Owner) + assert.Equal(t, []string{"SE_DACL_PRESENT", "SE_SELF_RELATIVE"}, obj.DescriptorFlags) + // The -l grammar does not use the compact RW form, so AccessEntries stays empty. + assert.Empty(t, obj.AccessEntries) + + require.Len(t, obj.DACL, 3) + + assert.Equal(t, 0, obj.DACL[0].Index) + assert.Equal(t, "ACCESS_ALLOWED_ACE_TYPE", obj.DACL[0].AceType) + assert.Equal(t, `NT AUTHORITY\SYSTEM`, obj.DACL[0].Principal) + assert.Empty(t, obj.DACL[0].AceFlags) + assert.Equal(t, []string{"SERVICE_QUERY_STATUS", "SERVICE_START", "READ_CONTROL"}, obj.DACL[0].Rights) + + assert.Equal(t, `BUILTIN\Administrators`, obj.DACL[1].Principal) + assert.Equal(t, []string{"SERVICE_ALL_ACCESS"}, obj.DACL[1].Rights) + + assert.Equal(t, 2, obj.DACL[2].Index) + assert.Equal(t, "ACCESS_DENIED_ACE_TYPE", obj.DACL[2].AceType) + assert.Equal(t, `NT AUTHORITY\NETWORK`, obj.DACL[2].Principal) + assert.Equal(t, []string{"INHERITED_ACE"}, obj.DACL[2].AceFlags) + assert.Equal(t, []string{"SERVICE_STOP"}, obj.DACL[2].Rights) + + require.Len(t, obj.SACL, 1) + assert.Equal(t, 0, obj.SACL[0].Index) + assert.Empty(t, obj.SACL[0].AceType) + assert.Equal(t, "Everyone", obj.SACL[0].Principal) + assert.Equal(t, []string{"FAILED_ACCESS_ACE_FLAG"}, obj.SACL[0].AceFlags) + assert.Equal(t, []string{"SERVICE_ALL_ACCESS"}, obj.SACL[0].Rights) +} + +func TestParse_Garbage(t *testing.T) { + data, err := os.ReadFile("./testdata/garbage.txt") + require.NoError(t, err) + + report, err := accesschk.Parse(data) + require.NoError(t, err) + assert.False(t, report.LooksLikeAccessChk()) +} + +func TestParse_InvalidUTF8(t *testing.T) { + _, err := accesschk.Parse([]byte{0xff, 0xfe, 0x00, 0x01}) + assert.Error(t, err) +} + +func TestParse_Empty(t *testing.T) { + report, err := accesschk.Parse([]byte{}) + require.NoError(t, err) + assert.False(t, report.LooksLikeAccessChk()) + assert.Empty(t, report.Objects) +} diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/default.txt b/pkg/attestation/crafter/materials/accesschk/testdata/default.txt new file mode 100644 index 000000000..cfd36ee0a --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/default.txt @@ -0,0 +1,8 @@ +Accesschk v6.15 - Reports effective permissions for securable objects +Copyright (C) 2006-2023 Mark Russinovich +Sysinternals - www.sysinternals.com + +c:\windows\system32\notepad.exe + RW NT AUTHORITY\SYSTEM + RW BUILTIN\Administrators + R BUILTIN\Users diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/descriptor.txt b/pkg/attestation/crafter/materials/accesschk/testdata/descriptor.txt new file mode 100644 index 000000000..54e8eafb2 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/descriptor.txt @@ -0,0 +1,18 @@ +ExampleService + DESCRIPTOR FLAGS: + [SE_DACL_PRESENT] + [SE_SELF_RELATIVE] + OWNER: NT AUTHORITY\SYSTEM + [0] ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\SYSTEM + SERVICE_QUERY_STATUS + SERVICE_START + READ_CONTROL + [1] ACCESS_ALLOWED_ACE_TYPE: BUILTIN\Administrators + SERVICE_ALL_ACCESS + [2] ACCESS_DENIED_ACE_TYPE: NT AUTHORITY\NETWORK + [INHERITED_ACE] + SERVICE_STOP + SACL: + [0] : Everyone + [FAILED_ACCESS_ACE_FLAG] + SERVICE_ALL_ACCESS diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/garbage.txt b/pkg/attestation/crafter/materials/accesschk/testdata/garbage.txt new file mode 100644 index 000000000..fb51654ff --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/garbage.txt @@ -0,0 +1,3 @@ +this is not accesschk output +just some random text +hello world diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/nobanner.txt b/pkg/attestation/crafter/materials/accesschk/testdata/nobanner.txt new file mode 100644 index 000000000..31d07421b --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/nobanner.txt @@ -0,0 +1,3 @@ +c:\windows\system32\notepad.exe + RW NT AUTHORITY\SYSTEM + R BUILTIN\Users diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/sddl.txt b/pkg/attestation/crafter/materials/accesschk/testdata/sddl.txt new file mode 100644 index 000000000..37af260e8 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/sddl.txt @@ -0,0 +1,15 @@ +Accesschk v6.15 - Reports effective permissions for securable objects +Copyright (C) 2006-2023 Mark Russinovich +Sysinternals - www.sysinternals.com + +c:\windows\system32\notepad.exe + DESCRIPTOR FLAGS: + SE_DACL_PRESENT + SE_SACL_PRESENT + SE_SELF_RELATIVE + OWNER: NT SERVICE\TrustedInstaller + [0] ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\SYSTEM + FILE_ALL_ACCESS + [1] ACCESS_ALLOWED_ACE_TYPE: BUILTIN\Users + FILE_GENERIC_READ + FILE_GENERIC_EXECUTE diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/service.txt b/pkg/attestation/crafter/materials/accesschk/testdata/service.txt new file mode 100644 index 000000000..8ca02830a --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/service.txt @@ -0,0 +1,10 @@ +Accesschk v6.15 - Reports effective permissions for securable objects +Copyright (C) 2006-2023 Mark Russinovich +Sysinternals - www.sysinternals.com + +spooler + RW NT AUTHORITY\SYSTEM + SERVICE_ALL_ACCESS + R NT AUTHORITY\INTERACTIVE + SERVICE_QUERY_STATUS + SERVICE_QUERY_CONFIG diff --git a/pkg/attestation/crafter/materials/accesschk/testdata/verbose.txt b/pkg/attestation/crafter/materials/accesschk/testdata/verbose.txt new file mode 100644 index 000000000..b40da9fab --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk/testdata/verbose.txt @@ -0,0 +1,11 @@ +Accesschk v6.15 - Reports effective permissions for securable objects +Copyright (C) 2006-2023 Mark Russinovich +Sysinternals - www.sysinternals.com + +c:\windows\system32\notepad.exe + RW NT AUTHORITY\SYSTEM + FILE_ALL_ACCESS + R BUILTIN\Users + FILE_EXECUTE + FILE_READ_ATTRIBUTES + FILE_READ_DATA diff --git a/pkg/attestation/crafter/materials/accesschk_test.go b/pkg/attestation/crafter/materials/accesschk_test.go new file mode 100644 index 000000000..a46827798 --- /dev/null +++ b/pkg/attestation/crafter/materials/accesschk_test.go @@ -0,0 +1,143 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package materials_test + +import ( + "context" + "testing" + + contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials" + "github.com/chainloop-dev/chainloop/pkg/casclient" + mUploader "github.com/chainloop-dev/chainloop/pkg/casclient/mocks" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNewAccessChkCrafter(t *testing.T) { + testCases := []struct { + name string + input *contractAPI.CraftingSchema_Material + wantErr bool + }{ + { + name: "happy path", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK, + }, + }, + { + name: "wrong type", + input: &contractAPI.CraftingSchema_Material{ + Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := materials.NewAccessChkCrafter(tc.input, nil, nil) + if tc.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestAccessChkCrafter_Craft(t *testing.T) { + testCases := []struct { + name string + filePath string + wantErr string + annotations map[string]string + }{ + { + name: "invalid path", + filePath: "./testdata/non-existing.txt", + wantErr: "can't open the file", + }, + { + name: "empty file", + filePath: "./testdata/empty.txt", + wantErr: "unexpected material type", + }, + { + name: "wrong content", + filePath: "./testdata/sbom-spdx.json", + wantErr: "unexpected material type", + }, + { + name: "default output with banner", + filePath: "./testdata/accesschk-default.txt", + annotations: map[string]string{ + "chainloop.material.tool.name": "AccessChk", + "chainloop.material.tool.version": "6.15", + }, + }, + { + name: "verbose output without banner", + filePath: "./testdata/accesschk-verbose.txt", + annotations: map[string]string{ + "chainloop.material.tool.name": "AccessChk", + }, + }, + } + + schema := &contractAPI.CraftingSchema_Material{ + Name: "test", + Type: contractAPI.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK, + } + + l := zerolog.Nop() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + uploader := mUploader.NewUploader(t) + if tc.wantErr == "" { + uploader.On("Upload", context.TODO(), mock.Anything, mock.Anything, mock.Anything). + Return(&casclient.UpDownStatus{}, nil) + } + + backend := &casclient.CASBackend{Uploader: uploader} + crafter, err := materials.NewAccessChkCrafter(schema, backend, &l) + require.NoError(t, err) + + got, err := crafter.Craft(context.TODO(), tc.filePath) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, contractAPI.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK.String(), got.MaterialType.String()) + assert.True(t, got.UploadedToCas) + + for k, v := range tc.annotations { + assert.Equal(t, v, got.Annotations[k]) + } + + // The verbose fixture has no banner, so no version annotation must be set. + if tc.name == "verbose output without banner" { + assert.Empty(t, got.Annotations["chainloop.material.tool.version"]) + } + }) + } +} diff --git a/pkg/attestation/crafter/materials/materials.go b/pkg/attestation/crafter/materials/materials.go index 28e8698df..4f162569b 100644 --- a/pkg/attestation/crafter/materials/materials.go +++ b/pkg/attestation/crafter/materials/materials.go @@ -301,6 +301,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia crafter, err = NewAsyncAPICrafter(materialSchema, casBackend, logger, WithAsyncAPINoStrictValidation(opts.NoStrictValidation)) case schemaapi.CraftingSchema_Material_GRAPHQL_SPEC: crafter, err = NewGraphQLCrafter(materialSchema, casBackend, logger) + case schemaapi.CraftingSchema_Material_SYSINTERNALS_ACCESSCHK: + crafter, err = NewAccessChkCrafter(materialSchema, casBackend, logger) default: return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type) } diff --git a/pkg/attestation/crafter/materials/testdata/accesschk-default.txt b/pkg/attestation/crafter/materials/testdata/accesschk-default.txt new file mode 100644 index 000000000..cfd36ee0a --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/accesschk-default.txt @@ -0,0 +1,8 @@ +Accesschk v6.15 - Reports effective permissions for securable objects +Copyright (C) 2006-2023 Mark Russinovich +Sysinternals - www.sysinternals.com + +c:\windows\system32\notepad.exe + RW NT AUTHORITY\SYSTEM + RW BUILTIN\Administrators + R BUILTIN\Users diff --git a/pkg/attestation/crafter/materials/testdata/accesschk-verbose.txt b/pkg/attestation/crafter/materials/testdata/accesschk-verbose.txt new file mode 100644 index 000000000..69d767f4d --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/accesschk-verbose.txt @@ -0,0 +1,6 @@ +c:\windows\system32\notepad.exe + RW NT AUTHORITY\SYSTEM + FILE_ALL_ACCESS + R BUILTIN\Users + FILE_EXECUTE + FILE_READ_DATA