blob: 0ada5c8f308ad3316a8c1bee779f32a687ef27fe [file] [edit]
// Copyright 2021 Google LLC
//
// 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 configs_test
import (
"embed"
"io/fs"
"log"
"path"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"bitbucket.org/creachadair/stringset"
cpb "github.com/google/localtoast/scannerlib/proto/compliance_go_proto"
spb "github.com/google/localtoast/scannerlib/proto/severity_go_proto"
apb "github.com/google/localtoast/scannerlib/proto/api_go_proto"
ipb "github.com/google/localtoast/scannerlib/proto/scan_instructions_go_proto"
)
//go:embed defs/*
var defEfs embed.FS
//go:embed reduced/*/*
var configEfs embed.FS
var configFileNames = stringset.New(
"vm_image_scanning.textproto", "container_image_scanning.textproto", "instance_scanning.textproto")
var scanConfigDefs, reducedScanConfigs = readConfigFiles()
func readConfigFiles() (map[string][]byte, map[string][]byte) {
return readFilesInDir(defEfs), readFilesInDir(configEfs)
}
func readFilesInDir(efs embed.FS) map[string][]byte {
result := make(map[string][]byte)
err := fs.WalkDir(efs, ".", func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if path.Ext(filePath) != ".textproto" {
return nil
}
content, err := efs.ReadFile(filePath)
if err != nil {
return err
}
result[filePath] = content
return nil
})
if err != nil {
log.Fatalf("Error reading scan config: %v\n", err)
}
return result
}
// Validate behavior across all the configs.
func TestRequiredAttributes(t *testing.T) {
for filePath, configBytes := range scanConfigDefs {
config := &apb.ScanConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Errorf("error reading %s: %v", filePath, err)
}
for _, b := range config.GetBenchmarkConfigs() {
noteID := b.GetId()
note := b.GetComplianceNote()
if note.GetTitle() == "" {
t.Errorf(`%s GetTitle(): got "", want non-empty string`, noteID)
}
if note.GetDescription() == "" {
t.Errorf(`%s GetDescription(): got "", want non-empty string`, noteID)
}
if note.GetRationale() == "" {
t.Errorf(`%s GetRationale(): got "", want non-empty string`, noteID)
}
if note.GetRemediation() == "" {
t.Errorf(`%s GetRemediation(): got "", want non-empty string`, noteID)
}
if len(note.GetVersion()) == 0 {
t.Errorf(`%s len(GetVersion()): got 0, want at least 1`, noteID)
}
for _, version := range note.GetVersion() {
if version.GetVersion() == "" {
t.Errorf(`%s version.GetVersion(): got "", want non-empty string`, noteID)
}
if version.GetCpeUri() != "fallback" && version.GetCpeUri() != "cpe:/distribution_independent_linux" && len(strings.Split(version.GetCpeUri(), ":")) > 5 {
t.Errorf("%s version.getCpeUri(): got %s, want a valid CPE", noteID, version.GetCpeUri())
}
}
if note.GetCisBenchmark().GetProfileLevel() < 1 || note.GetCisBenchmark().GetProfileLevel() > 3 {
t.Errorf("%s GetCisBenchmark().GetProfilelevel(): got %d, want 1 <= level <= 3", noteID, note.GetCisBenchmark().GetProfileLevel())
}
if note.GetCisBenchmark().GetSeverity() == spb.Severity_SEVERITY_UNSPECIFIED {
t.Errorf("%s GetCisBenchmark().GetSeverity(): got %s, want any specified severity", noteID, note.GetCisBenchmark().GetSeverity())
}
scanInstructions := &ipb.BenchmarkScanInstructionDef{}
if err := prototext.Unmarshal(note.GetScanInstructions(), scanInstructions); err != nil {
t.Errorf("%s could not parse scan instructions: %v", noteID, err)
}
}
}
}
func TestFilesHaveSupportedName(t *testing.T) {
for filePath := range reducedScanConfigs {
fileName := filepath.Base(filePath)
if !configFileNames.Contains(fileName) {
t.Errorf("checking file names: got %q, want image_scanning.textproto or instance_scanning.textproto", fileName)
}
}
}
func TestScanInstructionsHaveDisplayCommandAndNonComplianceReason(t *testing.T) {
for filePath, configBytes := range scanConfigDefs {
config := &apb.ScanConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Fatalf("error reading %s: %v", filePath, err)
}
for _, b := range config.GetBenchmarkConfigs() {
noteID := b.GetId()
instructionDef := &ipb.BenchmarkScanInstructionDef{}
if err := prototext.Unmarshal(b.GetComplianceNote().GetScanInstructions(), instructionDef); err != nil {
t.Errorf("%s could not parse scan instructions: %v", noteID, err)
}
var scanInstructions []*ipb.BenchmarkScanInstruction
if instructionDef.GetGeneric() != nil {
scanInstructions = []*ipb.BenchmarkScanInstruction{instructionDef.GetGeneric()}
} else if instructionDef.GetScanTypeSpecific() != nil {
scanInstructions = []*ipb.BenchmarkScanInstruction{
instructionDef.GetScanTypeSpecific().InstanceScanning,
instructionDef.GetScanTypeSpecific().ImageScanning,
}
} else {
t.Fatalf("benchmark %s has invalid instruction def %v", noteID, instructionDef)
}
for _, i := range scanInstructions {
for _, a := range i.GetCheckAlternatives() {
for _, f := range a.GetFileChecks() {
if len(f.GetFileDisplayCommand()) > 0 && len(f.GetNonComplianceMsg()) == 0 {
t.Errorf("check for benchmark %s has a file display command set but no non-compliance message: %v", noteID, f)
}
}
}
}
}
}
}
func TestFallbackBenchmarkDefsHaveExpectedIdFormat(t *testing.T) {
for filePath, configBytes := range scanConfigDefs {
if filePath != "fallback.textproto" {
continue
}
config := &apb.ScanConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Errorf("error reading %s: %v", filePath, err)
}
for _, b := range config.GetBenchmarkConfigs() {
noteID := b.GetId()
if !strings.HasSuffix(noteID, "-fallback") {
t.Errorf("Fallback benchmark ID %q should end with -fallback", noteID)
}
}
}
}
func TestFallbackPerOsBenchmarksHaveExpectedIdFormat(t *testing.T) {
for filePath, configBytes := range reducedScanConfigs {
if !strings.Contains(filePath, "/fallback/") {
continue
}
config := &apb.PerOsBenchmarkConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Errorf("error reading %s: %v", filePath, err)
}
for _, id := range config.BenchmarkId {
if !strings.HasSuffix(id, "-fallback") {
t.Errorf("Fallback benchmark ID %q should end with -fallback", id)
}
}
}
}
func TestProfileLevelOverridesUseExistingIDs(t *testing.T) {
for filePath, configBytes := range reducedScanConfigs {
config := &apb.PerOsBenchmarkConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Errorf("error reading %s: %v", filePath, err)
}
ids := make(map[string]bool)
for _, id := range config.BenchmarkId {
ids[id] = true
}
for _, o := range config.ProfileLevelOverride {
for _, id := range o.BenchmarkId {
if _, ok := ids[id]; !ok {
t.Errorf("%s: overridden benchmark ID %q isn't used by the config", filePath, id)
}
}
}
}
}
func TestConfigDefContainsUniqueNoteDefinitions(t *testing.T) {
for filePath, configBytes := range scanConfigDefs {
noteIdsToConfigs := make(map[string][]*apb.BenchmarkConfig)
config := &apb.ScanConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Errorf("error reading %s: %v", filePath, err)
}
for _, benchmarkConfig := range config.GetBenchmarkConfigs() {
noteID := benchmarkConfig.GetId()
if configs, present := noteIdsToConfigs[noteID]; present {
for _, priorBenchmarkConfig := range configs {
if diff := cmp.Diff(priorBenchmarkConfig, benchmarkConfig, protocmp.Transform(), protocmp.IgnoreFields(&cpb.ComplianceNote{}, "version")); diff == "" {
t.Errorf("%s: Benchmark ID %q has the exact definition specified multiple times", filePath, noteID)
}
}
noteIdsToConfigs[noteID] = append(configs, benchmarkConfig)
} else {
noteIdsToConfigs[noteID] = []*apb.BenchmarkConfig{benchmarkConfig}
}
}
}
}
func TestOnlyOneDelimiterForGivenFile(t *testing.T) {
fileToDelimiter := make(map[string]string)
for filePath, configBytes := range scanConfigDefs {
config := &apb.ScanConfig{}
if err := prototext.Unmarshal(configBytes, config); err != nil {
t.Fatalf("error reading %s: %v", filePath, err)
}
for _, b := range config.GetBenchmarkConfigs() {
noteID := b.GetId()
instructionDef := &ipb.BenchmarkScanInstructionDef{}
if err := prototext.Unmarshal(b.GetComplianceNote().GetScanInstructions(), instructionDef); err != nil {
t.Errorf("%s could not parse scan instructions: %v", noteID, err)
}
var scanInstructions []*ipb.BenchmarkScanInstruction
if instructionDef.GetGeneric() != nil {
scanInstructions = []*ipb.BenchmarkScanInstruction{instructionDef.GetGeneric()}
} else if instructionDef.GetScanTypeSpecific() != nil {
scanInstructions = []*ipb.BenchmarkScanInstruction{
instructionDef.GetScanTypeSpecific().InstanceScanning,
instructionDef.GetScanTypeSpecific().ImageScanning,
}
} else {
t.Fatalf("benchmark %s has invalid instruction def %v", noteID, instructionDef)
}
for _, i := range scanInstructions {
for _, a := range i.GetCheckAlternatives() {
for _, f := range a.GetFileChecks() {
if f.GetContentEntry() != nil {
delimiter := string(f.GetContentEntry().GetDelimiter())
if delimiter == "" {
delimiter = "\n" // Default value.
}
for _, fs := range f.GetFilesToCheck() {
fileToCheck, err := proto.MarshalOptions{Deterministic: true}.Marshal(fs)
if err != nil {
t.Fatalf("Error marshaling %v: %v", f.GetFilesToCheck(), err)
}
if prev, ok := fileToDelimiter[string(fileToCheck)]; ok {
if prev != delimiter {
t.Fatalf("file %v has content entry checks with different delimiters: %q vs. %q",
fs, prev, delimiter)
}
} else {
fileToDelimiter[string(fileToCheck)] = delimiter
}
}
}
}
}
}
}
}
}