blob: 88cc003a42306ff6648cf05a89b834b040fd2bc4 [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 scannerlib provides an interface for running compliance and config
// security checks on a machine.
package scannerlib
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
cpb "github.com/google/localtoast/scannerlib/proto/compliance_go_proto"
"github.com/google/localtoast/scanapi"
"github.com/google/localtoast/scannerlib/configchecks"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
apb "github.com/google/localtoast/scannerlib/proto/api_go_proto"
)
// Scanner is the main entry point of the scanner.
type Scanner struct{}
// Scan executes the scan for benchmark compliance using the provided scan
// config and API for accessing the scanned machine.
func (Scanner) Scan(ctx context.Context, config *apb.ScanConfig, api scanapi.ScanAPI) (*apb.ScanResults, error) {
if err := validateBenchmarkConfigs(config.GetBenchmarkConfigs()); err != nil {
return nil, err
}
scanStartTime := time.Now()
checks, err := configchecks.CreateChecksFromConfig(ctx, config, &apiErrorWrapper{api: api})
if err != nil {
return nil, err
}
checkResults, benchmarkErrors := executeChecks(checks)
complianceResults := determineBenchmarkCompliance(checks, checkResults, benchmarkErrors)
benchmarkVersion, err := oldestBenchmarkVersion(config.GetBenchmarkConfigs())
if err != nil {
fmt.Fprintf(os.Stderr, "Error while determining oldest benchmark version: %v\n", err)
benchmarkVersion = "0.0.0"
}
options := newScanResultsOptions{
startTime: scanStartTime,
benchmarkVersion: benchmarkVersion,
benchmarkDocument: getBenchmarkDocument(config.GetBenchmarkConfigs()),
compliantBenchmarks: complianceResults.compliantBenchmarks,
nonCompliantBenchmarks: complianceResults.nonCompliantBenchmarks,
}
if len(complianceResults.unknownBenchmarks) == 0 {
options.status = apb.ScanStatus_SUCCEEDED
} else {
options.status = apb.ScanStatus_FAILED
errorStrings := ""
for _, errs := range benchmarkErrors {
for _, err := range errs {
errorStrings += err.Error() + "\n"
}
}
options.failureReason = fmt.Sprintf(
"Compliance state of the following benchmarks couldn't be determined: [%s]\n"+
"The following errors were encountered while running the checks:\n%s",
strings.Join(complianceResults.unknownBenchmarks, ","), errorStrings)
}
return newScanResults(options), nil
}
func validateBenchmarkConfigs(configs []*apb.BenchmarkConfig) error {
ids := map[string]bool{}
for _, config := range configs {
id := config.GetId()
if ids[id] {
return fmt.Errorf("duplicate benchmark ID %s", id)
}
ids[id] = true
if err := configchecks.ValidateScanInstructions(config); err != nil {
return err
}
}
return nil
}
// executeChecks runs the given benchmarkChecks and returns their findings.
// It also returns a map of benchmark IDs to errors that the benchmark's checks
// produced while running, or an empty slice if no errors occurred for a benchmark.
func executeChecks(checks []configchecks.BenchmarkCheck) ([]*apb.ComplianceResult, map[string][]error) {
compliancePerAlternative := make(configchecks.ComplianceMap)
benchmarkErrors := make(map[string][]error)
for _, check := range checks {
checkResults, err := check.Exec()
if err != nil {
fmt.Fprintf(os.Stderr, "Error while executing check %s: %v\n", check, err)
}
for _, id := range check.BenchmarkIDs() {
if _, ok := benchmarkErrors[id]; !ok {
benchmarkErrors[id] = []error{}
}
if err != nil {
benchmarkErrors[id] = append(benchmarkErrors[id], fmt.Errorf("%s: %s", check, err))
}
}
// Merge the check results into a unified compliance map.
for altNum, compliance := range checkResults {
if prev, ok := compliancePerAlternative[altNum]; ok {
appendToComplianceResult(prev, compliance)
} else {
compliancePerAlternative[altNum] = compliance
}
}
}
result := make([]*apb.ComplianceResult, 0, len(compliancePerAlternative))
for _, c := range compliancePerAlternative {
result = append(result, c)
}
return result, benchmarkErrors
}
type benchmarkCompliance struct {
compliantBenchmarks []*apb.ComplianceResult
nonCompliantBenchmarks []*apb.ComplianceResult
unknownBenchmarks []string
}
// determineBenchmarkCompliance takes the results of the check runs and aggregates them to figure
// out which benchmarks were compliant, which weren't, and which benchmarks' compliance couldn't be
// determined.
func determineBenchmarkCompliance(checks []configchecks.BenchmarkCheck, checkResults []*apb.ComplianceResult, benchmarkErrors map[string][]error) *benchmarkCompliance {
complianceResultForBenchmark := make(map[string]*apb.ComplianceResult)
for _, c := range checkResults {
// A benchmark is compliant if any of its check alternatives is compliant.
if prev, ok := complianceResultForBenchmark[c.GetId()]; ok {
if isResultCompliant(prev) {
// Result is already compliant.
continue
}
if isResultCompliant(c) {
// Overwrite any previous non-compliant results.
complianceResultForBenchmark[c.GetId()] = c
} else {
// Merge the two non-compliant results.
appendToComplianceResult(prev, c)
}
} else {
complianceResultForBenchmark[c.GetId()] = c
}
}
result := &benchmarkCompliance{
compliantBenchmarks: []*apb.ComplianceResult{},
nonCompliantBenchmarks: []*apb.ComplianceResult{},
unknownBenchmarks: []string{},
}
for id, err := range benchmarkErrors {
if len(err) > 0 {
// Some checks haven't completed successfully, compliance can't be determined.
result.unknownBenchmarks = append(result.unknownBenchmarks, id)
continue
}
c := complianceResultForBenchmark[id]
if !isResultCompliant(c) {
// The benchmark is not compliant.
result.nonCompliantBenchmarks = append(result.nonCompliantBenchmarks, c)
} else {
// The benchmark is compliant.
result.compliantBenchmarks = append(result.compliantBenchmarks, c)
}
}
deduplicateNonCompliantFiles(result.nonCompliantBenchmarks)
return result
}
func isResultCompliant(c *apb.ComplianceResult) bool {
return len(c.GetComplianceOccurrence().NonCompliantFiles) == 0 && c.GetComplianceOccurrence().NonComplianceReason == ""
}
func appendToComplianceResult(currentResult, newResult *apb.ComplianceResult) {
current := currentResult.GetComplianceOccurrence()
new := newResult.GetComplianceOccurrence()
current.NonCompliantFiles = append(current.NonCompliantFiles, new.NonCompliantFiles...)
if new.NonComplianceReason != "" {
if current.NonComplianceReason != "" {
current.NonComplianceReason = current.NonComplianceReason + "\n" + new.NonComplianceReason
} else {
current.NonComplianceReason = new.NonComplianceReason
}
}
}
// deduplicateNonCompliantFiles removes duplicate non-compliant file entries from the
// given compliance findings.
func deduplicateNonCompliantFiles(compliances []*apb.ComplianceResult) {
type mapKey struct {
path string
displayCommand string
reason string
}
for _, c := range compliances {
occ := c.GetComplianceOccurrence()
fileMap := make(map[mapKey]bool)
for _, f := range occ.NonCompliantFiles {
fileMap[mapKey{
path: f.Path,
displayCommand: f.DisplayCommand,
reason: f.Reason,
}] = true
}
occ.NonCompliantFiles = make([]*cpb.NonCompliantFile, 0, len(fileMap))
for key, _ := range fileMap {
f := &cpb.NonCompliantFile{
Path: key.path,
Reason: key.reason,
DisplayCommand: key.displayCommand,
}
occ.NonCompliantFiles = append(occ.NonCompliantFiles, f)
}
// Keep the file list sorted for a better overview of non-compliance reports.
sort.Slice(occ.NonCompliantFiles, func(i, j int) bool {
f1 := occ.NonCompliantFiles[i]
f2 := occ.NonCompliantFiles[j]
if f1.Path != f2.Path {
return f1.Path < f2.Path
}
if f1.Reason != f2.Reason {
return f1.Reason < f2.Reason
}
return f1.DisplayCommand < f2.DisplayCommand
})
}
}
type newScanResultsOptions struct {
startTime time.Time
benchmarkVersion string
benchmarkDocument string
compliantBenchmarks []*apb.ComplianceResult
nonCompliantBenchmarks []*apb.ComplianceResult
status apb.ScanStatus_ScanStatusEnum
failureReason string
}
func newScanResults(options newScanResultsOptions) *apb.ScanResults {
return &apb.ScanResults{
StartTime: timestamppb.New(options.startTime),
EndTime: timestamppb.New(time.Now()),
ScannerVersion: ScannerVersion,
BenchmarkVersion: options.benchmarkVersion,
BenchmarkDocument: options.benchmarkDocument,
Status: &apb.ScanStatus{
Status: options.status,
FailureReason: options.failureReason,
},
CompliantBenchmarks: options.compliantBenchmarks,
NonCompliantBenchmarks: options.nonCompliantBenchmarks,
}
}
// oldestBenchmarkVersion returns the version of the oldest benchmark in the list. This value
// can be compared to the benchmark document version to figure out if a scan is up-to-date, i.e.
// if it was only using benchmarks defined in the latest document.
func oldestBenchmarkVersion(benchmarks []*apb.BenchmarkConfig) (string, error) {
oldest := ""
for _, benchmark := range benchmarks {
// A single benchmark can be defined in several documents. In this case we're interested
// in the version of the latest doc it's defined in.
newestInBenchmark := "0.0.0"
for _, version := range benchmark.GetComplianceNote().GetVersion() {
isNewer, err := isVersionNewer(version.GetVersion(), newestInBenchmark)
if err != nil {
return "", err
}
if isNewer {
newestInBenchmark = version.GetVersion()
}
}
if len(oldest) == 0 {
oldest = newestInBenchmark
} else {
isNewer, err := isVersionNewer(oldest, newestInBenchmark)
if err != nil {
return "", err
}
if isNewer {
oldest = newestInBenchmark
}
}
}
return oldest, nil
}
// getBenchmarkDocument returns name of the benchmark document used in the scan.
// This value is expected to be the same in all benchmarks from the config.
func getBenchmarkDocument(benchmarks []*apb.BenchmarkConfig) string {
if len(benchmarks) == 0 {
return ""
}
if len(benchmarks[0].GetComplianceNote().Version) == 0 {
return ""
}
return benchmarks[0].GetComplianceNote().Version[0].BenchmarkDocument
}
// isVersionNewer compares version strings that follow a num.num.num... pattern.
// It returns true if v1 is newer (a higher version) than v2, and false otherwise.
func isVersionNewer(v1, v2 string) (bool, error) {
v1Nums := strings.Split(v1, ".")
v2Nums := strings.Split(v2, ".")
if len(v1Nums) == 0 {
return false, fmt.Errorf("Invalid version string: %s", v1)
}
if len(v2Nums) == 0 {
return false, fmt.Errorf("Invalid version string: %s", v2)
}
numLength := len(v1Nums)
if numLength > len(v2Nums) {
numLength = len(v2Nums)
}
for i := 0; i < numLength; i++ {
v1Num, err := strconv.Atoi(v1Nums[i])
if err != nil {
return false, fmt.Errorf("Invalid version string: %s", v1)
}
v2Num, err := strconv.Atoi(v2Nums[i])
if err != nil {
return false, fmt.Errorf("Invalid version string: %s", v2)
}
if v1Num > v2Num {
return true, nil
}
if v1Num < v2Num {
return false, nil
}
}
return len(v1Nums) > len(v2Nums), nil
}