-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement system packages attestation for Debian and RPM backends
Signed-off-by: Frederick F. Kautz IV <[email protected]>
- Loading branch information
Showing
6 changed files
with
439 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,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) | ||
} |
This file contains 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,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 | ||
} |
This file contains 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,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 | ||
} |
This file contains 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,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 | ||
} |
This file contains 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
Oops, something went wrong.