Skip to content

Commit

Permalink
feat: Implement system packages attestation for Debian and RPM backends
Browse files Browse the repository at this point in the history
Signed-off-by: Frederick F. Kautz IV <[email protected]>
  • Loading branch information
fkautz committed Jan 27, 2025
1 parent a28a0c5 commit d2c65ff
Show file tree
Hide file tree
Showing 6 changed files with 439 additions and 0 deletions.
53 changes: 53 additions & 0 deletions attestation/system-packages/backends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2025 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 systempackages

import (
"os/exec"

"github.com/in-toto/go-witness/attestation"
)

type UbuntuBackend struct {
DebianBackend
}

func NewUbuntuBackend(osReleaseFile string) Backend {
return &UbuntuBackend{
DebianBackend: *NewDebianBackend(osReleaseFile).(*DebianBackend),
}
}

func (b *UbuntuBackend) RunType() attestation.RunType {
return RunType
}

type RedHatBackend struct {
RPMBackend
}

func NewRedHatBackend(osReleaseFile string) Backend {
return &RedHatBackend{
RPMBackend: *NewRPMBackend(osReleaseFile).(*RPMBackend),
}
}

func (b *RedHatBackend) RunType() attestation.RunType {
return RunType
}

func (b *RedHatBackend) SetExecCommand(cmd func(name string, arg ...string) *exec.Cmd) {
b.RPMBackend.SetExecCommand(cmd)
}
64 changes: 64 additions & 0 deletions attestation/system-packages/debian.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2025 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 systempackages

import (
"bufio"
"os/exec"
"strings"
)

type DebianBackend struct {
osReleaseFile string
execCommand func(name string, arg ...string) *exec.Cmd
}

func NewDebianBackend(osReleaseFile string) Backend {
return &DebianBackend{
osReleaseFile: osReleaseFile,
execCommand: exec.Command,
}
}

func (b *DebianBackend) SetExecCommand(cmd func(name string, arg ...string) *exec.Cmd) {
b.execCommand = cmd
}

func (b *DebianBackend) DetermineOSInfo() (string, string, string, error) {
return determineDistribution(b.osReleaseFile)
}

func (b *DebianBackend) GatherPackages() ([]Package, error) {
cmd := b.execCommand("dpkg-query", "-W", "-f", "${Package}\t${Version}\n")
output, err := cmd.Output()
if err != nil {
return nil, err
}

var packages []Package
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, "\t")
if len(parts) == 2 {
packages = append(packages, Package{
Name: parts[0],
Version: parts[1],
})
}
}

return packages, nil
}
110 changes: 110 additions & 0 deletions attestation/system-packages/rpm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2025 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 systempackages

import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"

"github.com/in-toto/go-witness/attestation"
)

type RPMBackend struct {
osReleaseFile string
execCommand func(name string, arg ...string) *exec.Cmd
}

func NewRPMBackend(osReleaseFile string) Backend {
return &RPMBackend{
osReleaseFile: osReleaseFile,
execCommand: exec.Command,
}
}

func (r *RPMBackend) DetermineOSInfo() (string, string, string, error) {
file, err := os.Open(r.osReleaseFile)
if err != nil {
return "", "", "", err
}
defer file.Close()

var distribution, version string

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")

switch key {
case "ID":
distribution = value
case "VERSION_ID":
version = value
}
}

if err := scanner.Err(); err != nil {
return "", "", "", err
}

return "linux", distribution, version, nil
}

func (r *RPMBackend) GatherPackages() ([]Package, error) {
cmd := r.execCommand("rpm", "-qa", "--qf", "%{NAME}\t%{VERSION}\n")
output, err := cmd.Output()
if err != nil {
return nil, err
}

fmt.Println("gather RPM packages:", string(output))

var packages []Package
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := scanner.Text()
parts := strings.Split(line, "\t")
if len(parts) == 2 {
packages = append(packages, Package{
Name: parts[0],
Version: parts[1],
})
}
}

if err := scanner.Err(); err != nil {
return nil, err
}

return packages, nil
}

// SetExecCommand allows setting a custom exec.Command function for testing
func (r *RPMBackend) SetExecCommand(cmd func(name string, arg ...string) *exec.Cmd) {
r.execCommand = cmd
}

// RunType returns the run type for the RPM backend
func (r *RPMBackend) RunType() attestation.RunType {
return RunType
}
161 changes: 161 additions & 0 deletions attestation/system-packages/system-packages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2025 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 systempackages

import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"

"github.com/in-toto/go-witness/attestation"
"github.com/invopop/jsonschema"
)

const (
Name = "system-packages"
Type = "https://witness.dev/attestations/system-packages/v0.1"
RunType = attestation.PreMaterialRunType
)

func init() {
attestation.RegisterAttestation(Name, Type, RunType, func() attestation.Attestor {
return NewSystemPackagesAttestor()
})
}

type Attestor struct {
OS string `json:"os"`
Distribution string `json:"distribution"`
Version string `json:"version"`
Packages []Package `json:"packages"`
backend Backend
}

type Package struct {
Name string `json:"name"`
Version string `json:"version"`
}

type Backend interface {
DetermineOSInfo() (string, string, string, error)
GatherPackages() ([]Package, error)
SetExecCommand(cmd func(name string, arg ...string) *exec.Cmd)
}

func NewSystemPackagesAttestor() *Attestor {
osReleaseFile := "/etc/os-release"
_, distribution, _, err := determineDistribution(osReleaseFile)
fmt.Println("discovered distribution:", distribution)
if err != nil {
// Default to Debian-based system if we can't determine the distribution
return &Attestor{
backend: NewDebianBackend(osReleaseFile),
}
}

switch distribution {
case "fedora", "rhel", "centos", "rocky", "alma", "oracle", "suse", "opensuse", "amazon":
return &Attestor{
backend: NewRPMBackend(osReleaseFile),
}
case "debian", "ubuntu":
return &Attestor{
backend: NewDebianBackend(osReleaseFile),
}
default:
// Use Debian backend for any other unrecognized distributions
return &Attestor{
backend: NewDebianBackend(osReleaseFile),
}
}
}

func determineDistribution(osReleaseFile string) (string, string, string, error) {
file, err := os.Open(osReleaseFile)
if err != nil {
return "", "", "", err
}
defer file.Close()

var distribution, version string

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")

switch key {
case "ID":
distribution = value
case "VERSION_ID":
version = value
}
}

if err := scanner.Err(); err != nil {
return "", "", "", err
}

return "linux", distribution, version, nil
}

// Attest implements attestation.Attestor.
func (a *Attestor) Attest(ctx *attestation.AttestationContext) error {
os, dist, version, err := a.backend.DetermineOSInfo()
fmt.Println(os)
fmt.Println(dist)
fmt.Println(version)
if err != nil {
return err
}
a.OS = os
a.Distribution = dist
a.Version = version

packages, err := a.backend.GatherPackages()
if err != nil {
return err
}
a.Packages = packages

return nil
}

// Name implements attestation.Attestor.
func (a *Attestor) Name() string {
return Name
}

// RunType implements attestation.Attestor.
func (a *Attestor) RunType() attestation.RunType {
return RunType
}

// Schema implements attestation.Attestor.
func (a *Attestor) Schema() *jsonschema.Schema {
return jsonschema.Reflect(a)
}

// Type implements attestation.Attestor.
func (a *Attestor) Type() string {
return Type
}
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
_ "github.com/in-toto/go-witness/attestation/sarif"
_ "github.com/in-toto/go-witness/attestation/sbom"
_ "github.com/in-toto/go-witness/attestation/slsa"
_ "github.com/in-toto/go-witness/attestation/system-packages"
_ "github.com/in-toto/go-witness/attestation/vex"

// signer providers
Expand Down
Loading

0 comments on commit d2c65ff

Please sign in to comment.