From 959388707aa3804ee1234d7d075e90bc134553b3 Mon Sep 17 00:00:00 2001 From: "Frederick F. Kautz IV" Date: Mon, 7 Oct 2024 20:53:39 -0700 Subject: [PATCH] feat: Add lockfile attestor This commit introduces a new lockfiles attestor to capture and attest the contents of common lockfiles in the project. The changes include: - Add new file attestation/lockfiles/lockfiles.go implementing the lockfiles attestor - Update imports.go to include the new lockfiles package The lockfiles attestor captures contents of various lockfiles such as Gemfile.lock, package-lock.json, yarn.lock, and others. It stores the information in a slice of LockfileInfo structs, allowing for flexible handling of multiple lockfiles. This feature enhances the project's capability to track and verify dependency information as part of the attestation process." Signed-off-by: Frederick F. Kautz IV --- attestation/lockfiles/lockfiles.go | 113 ++++++++++++++++++++++++ attestation/lockfiles/lockfiles_test.go | 99 +++++++++++++++++++++ imports.go | 1 + schemagen/lockfiles.json | 37 ++++++++ 4 files changed, 250 insertions(+) create mode 100644 attestation/lockfiles/lockfiles.go create mode 100644 attestation/lockfiles/lockfiles_test.go create mode 100644 schemagen/lockfiles.json diff --git a/attestation/lockfiles/lockfiles.go b/attestation/lockfiles/lockfiles.go new file mode 100644 index 00000000..c1104473 --- /dev/null +++ b/attestation/lockfiles/lockfiles.go @@ -0,0 +1,113 @@ +// Copyright 2024 The Witness Contributors +// +// 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 lockfiles + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/invopop/jsonschema" + + "github.com/in-toto/go-witness/attestation" +) + +const ( + Name = "lockfiles" + Type = "https://witness.dev/attestations/lockfiles/v0.1" + RunType = attestation.PreMaterialRunType +) + +func init() { + attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor { + return NewLockfilesAttestor() + }) +} + +func NewLockfilesAttestor() attestation.Attestor { + return &Attestor{ + Lockfiles: []LockfileInfo{}, + } +} + +// Attestor implements the lockfiles attestation type +type Attestor struct { + Lockfiles []LockfileInfo `json:"lockfiles"` +} + +// LockfileInfo stores information about a lockfile +type LockfileInfo struct { + Filename string `json:"filename"` + Content string `json:"content"` +} + +// Name returns the name of the attestation type +func (a *Attestor) Name() string { + return "lockfiles" +} + +// Attest captures the contents of common lockfiles +func (a *Attestor) Attest(ctx *attestation.AttestationContext) error { + lockfilePatterns := []string{ + "Gemfile.lock", // Ruby + "package-lock.json", // Node.js (npm) + "yarn.lock", // Node.js (Yarn) + "Cargo.lock", // Rust + "poetry.lock", // Python (Poetry) + "Pipfile.lock", // Python (Pipenv) + "composer.lock", // PHP + "go.sum", // Go + "Podfile.lock", // iOS/macOS (CocoaPods) + "gradle.lockfile", // Gradle + "pnpm-lock.yaml", // Node.js (pnpm) + } + + a.Lockfiles = []LockfileInfo{} + + for _, pattern := range lockfilePatterns { + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("error searching for %s: %w", pattern, err) + } + + for _, match := range matches { + content, err := os.ReadFile(match) + if err != nil { + return fmt.Errorf("error reading %s: %w", match, err) + } + a.Lockfiles = append(a.Lockfiles, LockfileInfo{ + Filename: filepath.Base(match), + Content: string(content), + }) + } + } + + return nil +} + +// RunType implements attestation.Attestor. +func (o *Attestor) RunType() attestation.RunType { + return RunType +} + +// // Schema implements attestation.Attestor. +func (o *Attestor) Schema() *jsonschema.Schema { + return jsonschema.Reflect(&o) +} + +// Type implements attestation.Attestor. +func (o *Attestor) Type() string { + return Type +} diff --git a/attestation/lockfiles/lockfiles_test.go b/attestation/lockfiles/lockfiles_test.go new file mode 100644 index 00000000..b7ba0448 --- /dev/null +++ b/attestation/lockfiles/lockfiles_test.go @@ -0,0 +1,99 @@ +// Copyright 2024 The Witness Contributors +// +// 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 lockfiles + +import ( + "os" + "path/filepath" + "testing" + + "github.com/in-toto/go-witness/attestation" +) + +func TestAttestor_Attest(t *testing.T) { + // Create a temporary directory for test files + tempDir, err := os.MkdirTemp("", "lockfiles_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Create test lockfiles + testFiles := map[string]string{ + "Gemfile.lock": "test content for Gemfile.lock", + "package-lock.json": "test content for package-lock.json", + } + + for filename, content := range testFiles { + err := os.WriteFile(filepath.Join(tempDir, filename), []byte(content), 0644) + if err != nil { + t.Fatalf("Failed to create test file %s: %v", filename, err) + } + } + + // Change to the temp directory + oldWd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current working directory: %v", err) + } + defer func() { + if err := os.Chdir(oldWd); err != nil { + t.Errorf("Failed to change back to original directory: %v", err) + } + }() + + err = os.Chdir(tempDir) + if err != nil { + t.Fatalf("Failed to change to temp directory: %v", err) + } + + // Create an Attestor and AttestationContext + attestor := &Attestor{} + ctx := &attestation.AttestationContext{} + + // Run the Attest method + err = attestor.Attest(ctx) + if err != nil { + t.Fatalf("Attest failed: %v", err) + } + + // Check if the lockfiles were captured correctly + if len(attestor.Lockfiles) != len(testFiles) { + t.Errorf("Expected %d lockfiles, but got %d", len(testFiles), len(attestor.Lockfiles)) + } + + for _, lockfile := range attestor.Lockfiles { + expectedContent, ok := testFiles[lockfile.Filename] + if !ok { + t.Errorf("Unexpected lockfile %s found in attestation", lockfile.Filename) + } else if lockfile.Content != expectedContent { + t.Errorf("Lockfile %s content mismatch. Got %s, want %s", lockfile.Filename, lockfile.Content, expectedContent) + } + delete(testFiles, lockfile.Filename) + } + + if len(testFiles) > 0 { + for filename := range testFiles { + t.Errorf("Expected lockfile %s not found in attestation", filename) + } + } +} + +func TestAttestor_Name(t *testing.T) { + attestor := &Attestor{} + if name := attestor.Name(); name != "lockfiles" { + t.Errorf("Incorrect attestor name. Got %s, want lockfiles", name) + } +} diff --git a/imports.go b/imports.go index 4db6e32c..f17869ef 100644 --- a/imports.go +++ b/imports.go @@ -27,6 +27,7 @@ import ( _ "github.com/in-toto/go-witness/attestation/jenkins" _ "github.com/in-toto/go-witness/attestation/jwt" _ "github.com/in-toto/go-witness/attestation/link" + _ "github.com/in-toto/go-witness/attestation/lockfiles" _ "github.com/in-toto/go-witness/attestation/material" _ "github.com/in-toto/go-witness/attestation/maven" _ "github.com/in-toto/go-witness/attestation/oci" diff --git a/schemagen/lockfiles.json b/schemagen/lockfiles.json new file mode 100644 index 00000000..4407b86d --- /dev/null +++ b/schemagen/lockfiles.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/Attestor", + "$defs": { + "Attestor": { + "properties": { + "lockfiles": { + "items": { + "$ref": "#/$defs/LockfileInfo" + }, + "type": "array" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "lockfiles" + ] + }, + "LockfileInfo": { + "properties": { + "filename": { + "type": "string" + }, + "content": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "filename", + "content" + ] + } + } +} \ No newline at end of file