Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
perf(crafter): de-duplicate AccessChk security descriptors in the policy projection#3364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+312
−1
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
14 changes: 13 additions & 1 deletion
14 pkg/attestation/crafter/api/attestation/v1/crafting_state.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143 pkg/attestation/crafter/materials/accesschk/projection.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 accesschk | ||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "reflect" | ||
| ) | ||
| // Projection is the JSON structure handed to the policy engine at evaluation | ||
| // time. It carries exactly the same information as a Report, but the security | ||
| // descriptors are de-duplicated: every distinct descriptor (the access-control | ||
| // portion of an object) is listed once in Descriptors, and each object references | ||
| // one by index. AccessChk evidence for a registry hive or a service database | ||
| // applies a handful of distinct descriptors to hundreds of thousands of objects | ||
| // through inheritance, so the flat form repeats the same DACL/ACE structures over | ||
| // and over. De-duplicating makes the projection — and the value the policy engine | ||
| // materialises from it — proportional to the number of DISTINCT descriptors | ||
| // rather than the number of objects, without dropping or altering any object, | ||
| // name, or ACE, so policy findings are unchanged. | ||
| // | ||
| // Policies read a descriptor via input.descriptors[obj.descriptor]. See the | ||
| // windows-*-strong-acls policies in the compliance-manifests repository. | ||
| type Projection struct { | ||
| Tool Tool `json:"tool"` | ||
| Descriptors []Descriptor `json:"descriptors"` | ||
| Objects []ProjectedObject `json:"objects"` | ||
| // Raw holds the full original text for inputs below RawRetentionLimit and is | ||
| // empty otherwise, mirroring Report.Raw. It is a string-matching fallback and | ||
| // is not read by current policies. | ||
| Raw string `json:"raw"` | ||
| } | ||
| // Descriptor is the security-descriptor portion of an Object: the fields that are | ||
| // shared between objects with identical access control. The object name and the | ||
| // verbatim raw lines are intentionally excluded — they belong to the object, not | ||
| // the descriptor. | ||
| type Descriptor struct { | ||
| 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"` | ||
| } | ||
| // ProjectedObject is a securable object in the de-duplicated projection: its name | ||
| // and an index into Projection.Descriptors, plus the verbatim RawLines fallback | ||
| // when retained (omitted for oversized inputs, matching Report). | ||
| type ProjectedObject struct { | ||
| Name string `json:"name"` | ||
| Descriptor int `json:"descriptor"` | ||
| RawLines []string `json:"raw_lines,omitempty"` | ||
| } | ||
| // Project converts a parsed Report into its de-duplicated Projection. Two objects | ||
| // share a descriptor entry only when their descriptor fields are byte-for-byte | ||
| // identical, so no information is lost: every object keeps its own name and its | ||
| // exact descriptor, and the mapping is fully reconstructable. | ||
| func (r *Report) Project() (*Projection, error) { | ||
| p := &Projection{ | ||
| Tool: r.Tool, | ||
| Descriptors: make([]Descriptor, 0), | ||
| Objects: make([]ProjectedObject, 0, len(r.Objects)), | ||
| Raw: r.Raw, | ||
| } | ||
| // index buckets descriptor positions by a cheap 64-bit fingerprint of their | ||
| // canonical JSON. A fingerprint collision is resolved by a full structural | ||
| // comparison, so distinct descriptors are never merged (no information lost). | ||
| // The serialization reuses a single buffer instead of allocating a fresh key | ||
| // per object, which for a hundreds-of-thousands-object material avoids the | ||
| // same order of transient garbage as the whole flat projection. | ||
| index := make(map[uint64][]int) | ||
| var buf bytes.Buffer | ||
| enc := json.NewEncoder(&buf) | ||
| for i := range r.Objects { | ||
| o := &r.Objects[i] | ||
| d := Descriptor{ | ||
| DescriptorFlags: o.DescriptorFlags, | ||
| Owner: o.Owner, | ||
| DACL: o.DACL, | ||
| SACL: o.SACL, | ||
| AccessEntries: o.AccessEntries, | ||
| } | ||
| buf.Reset() | ||
| if err := enc.Encode(&d); err != nil { | ||
| return nil, err | ||
| } | ||
| fp := fnv64a(buf.Bytes()) | ||
| idx := -1 | ||
| for _, cand := range index[fp] { | ||
| if reflect.DeepEqual(p.Descriptors[cand], d) { | ||
| idx = cand | ||
| break | ||
| } | ||
| } | ||
| if idx < 0 { | ||
| idx = len(p.Descriptors) | ||
| p.Descriptors = append(p.Descriptors, d) | ||
| index[fp] = append(index[fp], idx) | ||
| } | ||
| p.Objects = append(p.Objects, ProjectedObject{ | ||
| Name: o.Name, | ||
| Descriptor: idx, | ||
| RawLines: o.RawLines, | ||
| }) | ||
| } | ||
| return p, nil | ||
| } | ||
| // fnv64a is the 64-bit FNV-1a hash, inlined to fingerprint a byte slice without | ||
| // allocating a hash.Hash. It is used only to bucket candidate descriptors; | ||
| // equality is always confirmed structurally, so collisions are harmless. | ||
| func fnv64a(b []byte) uint64 { | ||
| const ( | ||
| offset = 14695981039346656037 | ||
| prime = 1099511628211 | ||
| ) | ||
| h := uint64(offset) | ||
| for _, c := range b { | ||
| h ^= uint64(c) | ||
| h *= prime | ||
| } | ||
| return h | ||
| } |
156 changes: 156 additions & 0 deletions
156 pkg/attestation/crafter/materials/accesschk/projection_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| // | ||
| // 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 | ||
| import ( | ||
| "encoding/json" | ||
| "strings" | ||
| "testing" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
| const ( | ||
| everyone = "Everyone" | ||
| keyAllAccess = "KEY_ALL_ACCESS" | ||
| ) | ||
| // Project must be lossless: every object's descriptor is exactly reconstructable | ||
| // from the descriptors table, and identical descriptors are shared. | ||
| func TestProjectIsLossless(t *testing.T) { | ||
| const sys = "SYSTEM" | ||
| report := &Report{ | ||
| Tool: Tool{Name: ToolName, Version: "6.15"}, | ||
| Objects: []Object{ | ||
| {Name: "key1", Owner: sys, DACL: []ACE{{Index: 0, Principal: everyone, Rights: []string{keyAllAccess}}}, AccessEntries: []AccessEntry{}}, | ||
| {Name: "key2", Owner: sys, DACL: []ACE{{Index: 0, Principal: everyone, Rights: []string{keyAllAccess}}}, AccessEntries: []AccessEntry{}}, // same descriptor as key1 | ||
| {Name: "key3", Owner: "Admins", DACL: []ACE{{Index: 0, Principal: sys, Rights: []string{"KEY_READ"}}}, AccessEntries: []AccessEntry{}}, // distinct | ||
| }, | ||
| } | ||
| proj, err := report.Project() | ||
| require.NoError(t, err) | ||
| // dedup happened: 3 objects, 2 distinct descriptors. | ||
| assert.Len(t, proj.Objects, 3) | ||
| assert.Len(t, proj.Descriptors, 2) | ||
| assert.Equal(t, proj.Objects[0].Descriptor, proj.Objects[1].Descriptor, "identical descriptors must share an index") | ||
| assert.NotEqual(t, proj.Objects[0].Descriptor, proj.Objects[2].Descriptor) | ||
| // every object reconstructs to its original descriptor, byte-for-byte. | ||
| for i, po := range proj.Objects { | ||
| orig := report.Objects[i] | ||
| d := proj.Descriptors[po.Descriptor] | ||
cubic-dev-ai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| assert.Equal(t, orig.Name, po.Name) | ||
| assert.Equal(t, orig.Owner, d.Owner) | ||
| assert.Equal(t, orig.DescriptorFlags, d.DescriptorFlags) | ||
| assert.Equal(t, orig.DACL, d.DACL) | ||
| assert.Equal(t, orig.SACL, d.SACL) | ||
| assert.Equal(t, orig.AccessEntries, d.AccessEntries) | ||
| } | ||
| } | ||
| // A parsed report projected must reconstruct the exact same set of (name, | ||
| // descriptor) pairs as the flat objects, over real AccessChk text. | ||
| func TestProjectRoundTripFromParse(t *testing.T) { | ||
| const sample = `accesschk v6.15 | ||
| HKLM\SOFTWARE\A | ||
| DESCRIPTOR FLAGS: | ||
| [SE_DACL_PRESENT] | ||
| OWNER: NT AUTHORITY\SYSTEM | ||
| [0] ACCESS_ALLOWED_ACE_TYPE: Everyone | ||
| KEY_ALL_ACCESS | ||
| HKLM\SOFTWARE\B | ||
| DESCRIPTOR FLAGS: | ||
| [SE_DACL_PRESENT] | ||
| OWNER: NT AUTHORITY\SYSTEM | ||
| [0] ACCESS_ALLOWED_ACE_TYPE: Everyone | ||
| KEY_ALL_ACCESS | ||
| ` | ||
| report, err := Parse(strings.NewReader(sample)) | ||
| require.NoError(t, err) | ||
| require.Len(t, report.Objects, 2) | ||
| proj, err := report.Project() | ||
| require.NoError(t, err) | ||
| require.Len(t, proj.Objects, 2) | ||
| // A and B share an identical descriptor. | ||
| assert.Len(t, proj.Descriptors, 1) | ||
| assert.Equal(t, proj.Objects[0].Descriptor, proj.Objects[1].Descriptor) | ||
| for i, po := range proj.Objects { | ||
| d := proj.Descriptors[po.Descriptor] | ||
| assert.Equal(t, report.Objects[i].Name, po.Name) | ||
| assert.Equal(t, report.Objects[i].DACL, d.DACL) | ||
| assert.Equal(t, report.Objects[i].Owner, d.Owner) | ||
| } | ||
| } | ||
| // The serialized projection is the contract the policy engine consumes, so assert | ||
| // the marshaled JSON shape directly: the descriptors table, objects that carry a | ||
| // "descriptor" index (and no inline DACL), the retained/omitted raw_lines | ||
| // fallback, and the top-level raw field. | ||
| func TestProjectJSONShape(t *testing.T) { | ||
| report := &Report{ | ||
| Tool: Tool{Name: ToolName, Version: "6.15"}, | ||
| Raw: "verbatim text", | ||
| Objects: []Object{ | ||
| {Name: "key1", DACL: []ACE{{Index: 0, AceType: "access_allowed_ace_type", Principal: everyone, Rights: []string{keyAllAccess}}}, AccessEntries: []AccessEntry{}, RawLines: []string{" raw line"}}, | ||
| {Name: "key2", DACL: []ACE{{Index: 0, AceType: "access_allowed_ace_type", Principal: everyone, Rights: []string{keyAllAccess}}}, AccessEntries: []AccessEntry{}}, // same descriptor, no raw lines | ||
| }, | ||
| } | ||
| proj, err := report.Project() | ||
| require.NoError(t, err) | ||
| b, err := json.Marshal(proj) | ||
| require.NoError(t, err) | ||
| // Decode into a struct mirroring the wire contract policies rely on. | ||
| var wire struct { | ||
| Tool map[string]any `json:"tool"` | ||
| Descriptors []struct { | ||
| DACL []map[string]any `json:"dacl"` | ||
| AccessEntries []any `json:"access_entries"` | ||
| } `json:"descriptors"` | ||
| Objects []struct { | ||
| Name string `json:"name"` | ||
| Descriptor *int `json:"descriptor"` | ||
| RawLines []string `json:"raw_lines"` | ||
| } `json:"objects"` | ||
| Raw string `json:"raw"` | ||
| } | ||
| require.NoError(t, json.Unmarshal(b, &wire)) | ||
| assert.Equal(t, "verbatim text", wire.Raw) | ||
| require.Len(t, wire.Descriptors, 1) // identical descriptors shared | ||
| require.Len(t, wire.Objects, 2) | ||
| require.NotNil(t, wire.Objects[0].Descriptor) | ||
| assert.Equal(t, "key1", wire.Objects[0].Name) | ||
| assert.Equal(t, *wire.Objects[0].Descriptor, *wire.Objects[1].Descriptor) | ||
| assert.Equal(t, everyone, wire.Descriptors[*wire.Objects[0].Descriptor].DACL[0]["principal"]) | ||
| assert.NotNil(t, wire.Descriptors[0].AccessEntries) | ||
| // raw_lines is retained for key1 and omitted for key2. | ||
| assert.Equal(t, []string{" raw line"}, wire.Objects[0].RawLines) | ||
| assert.Nil(t, wire.Objects[1].RawLines) | ||
| // Objects reference a descriptor index and never carry the DACL inline. | ||
| js := string(b) | ||
| assert.Contains(t, js, `"name":"key2","descriptor":0}`) | ||
| assert.NotContains(t, js, `"name":"key2","descriptor":0,"dacl"`) | ||
| } | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.