diff --git a/go.mod b/go.mod index 9ec8a72..dda9c38 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,9 @@ go 1.23.2 require ( github.com/chris-cmsoft/conftojson v0.0.0-20241105132434-7c8873e5cbb6 - github.com/compliance-framework/agent v0.1.5 - github.com/compliance-framework/configuration-service v0.1.3 - github.com/google/uuid v1.6.0 + github.com/compliance-framework/agent v0.1.7-0.20250407034843-719eb31e8190 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.2 - google.golang.org/protobuf v1.36.2 ) require ( @@ -17,6 +14,7 @@ require ( github.com/agnivade/levenshtein v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/compliance-framework/configuration-service v0.1.3 // indirect github.com/fatih/color v1.18.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -24,6 +22,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -51,6 +50,7 @@ require ( golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/grpc v1.69.2 // indirect + google.golang.org/protobuf v1.36.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 4c6123f..2509027 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chris-cmsoft/conftojson v0.0.0-20241105132434-7c8873e5cbb6 h1:50LCDU9IpLMdBC9+6+g7EReo50gr0BBFevGRaLwmdJQ= github.com/chris-cmsoft/conftojson v0.0.0-20241105132434-7c8873e5cbb6/go.mod h1:lGxtdIwiDsj1U+vUxAydlacBWodyOoBjgJ9IX6T8YTQ= -github.com/compliance-framework/agent v0.1.5 h1:kKRpY1Rado4AX0m1C3+wMpDjrbxn98AdmcrmXQUBkbo= -github.com/compliance-framework/agent v0.1.5/go.mod h1:4/NXMHo9zB8JHSDz9pXsCAaO6mzspjfL2hrWZSW2rjA= +github.com/compliance-framework/agent v0.1.7-0.20250407034843-719eb31e8190 h1:w3+1tky9Nnt2zFJFpYwMm14SchOu8FaX+4s29VcSDNI= +github.com/compliance-framework/agent v0.1.7-0.20250407034843-719eb31e8190/go.mod h1:dAjHJecMEMdKzylOatT54tXmbjgx5d2Wsq4mGnRksZk= github.com/compliance-framework/configuration-service v0.1.3 h1:Gf9/v+4IAHG2iVh+gZSiboMH7tAsVBn4FtnJZZsQIrU= github.com/compliance-framework/configuration-service v0.1.3/go.mod h1:tLKJKXbQbY9Pg/e3BJtJVkqxaejXJMHoE8Yp0NW4lDE= github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= @@ -320,5 +320,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go index ce4ead5..7c3ff74 100644 --- a/main.go +++ b/main.go @@ -3,19 +3,14 @@ package main import ( "context" "errors" - "fmt" "github.com/chris-cmsoft/cf-plugin-local-ssh/internal" - "github.com/google/uuid" - "os" - "time" - policyManager "github.com/compliance-framework/agent/policy-manager" "github.com/compliance-framework/agent/runner" "github.com/compliance-framework/agent/runner/proto" - "github.com/compliance-framework/configuration-service/sdk" "github.com/hashicorp/go-hclog" goplugin "github.com/hashicorp/go-plugin" - "google.golang.org/protobuf/types/known/timestamppb" + "os" + "slices" ) type LocalSSH struct { @@ -59,7 +54,6 @@ func (l *LocalSSH) Eval(req *proto.EvalRequest, apiHelper runner.ApiHelper) (*pr } func (l *LocalSSH) EvaluatePolicies(ctx context.Context, sshFetcher internal.SSHFetcher, req *proto.EvalRequest) ([]*proto.Observation, []*proto.Finding, error) { - startTime := time.Now() var accumulatedErrors error activities := make([]*proto.Activity, 0) @@ -80,197 +74,81 @@ func (l *LocalSSH) EvaluatePolicies(ctx context.Context, sshFetcher internal.SSH return observations, findings, accumulatedErrors } - for _, policyPath := range req.GetPolicyPaths() { - // Explicitly reset steps to make things readable - steps := make([]*proto.Step, 0) - steps = append(steps, &proto.Step{ - Title: "Compile policy bundle", - Description: "Using a locally addressable policy path, compile the policy files to an in memory executable.", - }) - steps = append(steps, &proto.Step{ - Title: "Execute policy bundle", - Description: "Using previously collected JSON-formatted SSH configuration, execute the compiled policies", - }) - results, err := policyManager.New(ctx, l.logger, policyPath).Execute(ctx, "local_ssh", sshConfigMap) - if err != nil { - l.logger.Error("Failed to evaluate against policy bundle", "error", err) - accumulatedErrors = errors.Join(accumulatedErrors, err) - return observations, findings, accumulatedErrors - } - - activities = append(activities, &proto.Activity{ - Title: "Execute policy", - Description: "Prepare and compile policy bundles, and execute them using the prepared SSH configuration data", - Steps: steps, - }) - - l.logger.Debug("local ssh evaluation completed", "results", results) - hostname := os.Getenv("HOSTNAME") - subjectAttributeMap := map[string]string{ - "type": "machine-instance", - "hostname": hostname, - } - subjects := []*proto.SubjectReference{ - { - Type: "machine-instance", - Attributes: subjectAttributeMap, - Title: internal.StringAddressed("Machine Instance"), - Remarks: internal.StringAddressed("A machine instance running the SSH software for remote access."), - Props: []*proto.Property{ - { - Name: "hostname", - Value: hostname, - Remarks: internal.StringAddressed("The local hostname of the machine where the plugin has been executed"), - }, + hostname := os.Getenv("HOSTNAME") + subjects := []*proto.SubjectReference{ + { + Type: "machine-instance", + Attributes: map[string]string{ + "type": "machine-instance", + "hostname": hostname, + }, + Title: policyManager.Pointer("Machine Instance"), + Remarks: policyManager.Pointer("A machine instance running the SSH software for remote access."), + Props: []*proto.Property{ + { + Name: "hostname", + Value: hostname, + Remarks: policyManager.Pointer("The local hostname of the machine where the plugin has been executed"), }, }, - } - actors := []*proto.OriginActor{ - { - Title: "The Continuous Compliance Framework", - Type: "assessment-platform", - Links: []*proto.Link{ - { - Href: "https://compliance-framework.github.io/docs/", - Rel: internal.StringAddressed("reference"), - Text: internal.StringAddressed("The Continuous Compliance Framework"), - }, + }, + } + actors := []*proto.OriginActor{ + { + Title: "The Continuous Compliance Framework", + Type: "assessment-platform", + Links: []*proto.Link{ + { + Href: "https://compliance-framework.github.io/docs/", + Rel: internal.StringAddressed("reference"), + Text: internal.StringAddressed("The Continuous Compliance Framework"), }, - Props: nil, }, - { - Title: "Continuous Compliance Framework - Local SSH Plugin", - Type: "tool", - Links: []*proto.Link{ - { - Href: "https://github.com/compliance-framework/plugin-local-ssh", - Rel: internal.StringAddressed("reference"), - Text: internal.StringAddressed("The Continuous Compliance Framework' Local SSH Plugin"), - }, + Props: nil, + }, + { + Title: "Continuous Compliance Framework - Local SSH Plugin", + Type: "tool", + Links: []*proto.Link{ + { + Href: "https://github.com/compliance-framework/plugin-local-ssh", + Rel: internal.StringAddressed("reference"), + Text: internal.StringAddressed("The Continuous Compliance Framework' Local SSH Plugin"), }, - Props: nil, }, - } - components := []*proto.ComponentReference{ - { - Identifier: "common-components/ssh", + Props: nil, + }, + } + components := []*proto.ComponentReference{ + { + Identifier: "common-components/ssh", + }, + } + + for _, policyPath := range req.GetPolicyPaths() { + // Explicitly reset steps to make things readable + processor := policyManager.NewPolicyProcessor( + l.logger, + map[string]string{ + "type": "machine-instance", + "hostname": hostname, + "_policy_path": policyPath, }, + subjects, + components, + actors, + ) + obs, finds, err := processor.GenerateResults(ctx, policyPath, sshConfigMap) + observations = slices.Concat(observations, obs) + findings = slices.Concat(findings, finds) + if err != nil { + accumulatedErrors = errors.Join(accumulatedErrors, err) } + } - activities = append(activities, &proto.Activity{ - Title: "Compile Results", - Description: "Using the output from policy execution, compile the resulting output to Observations and Findings, marking any violations, risks, and other OSCAL-familiar data", - Steps: steps, - }) - for _, result := range results { - // Observation UUID should differ for each individual subject, but remain consistent when validating the same policy for the same subject. - // This acts as an identifier to show the history of an observation. - observationUUIDMap := internal.MergeMaps(subjectAttributeMap, map[string]string{ - "type": "observation", - "policy": result.Policy.Package.PurePackage(), - "policy_file": result.Policy.File, - "policy_path": policyPath, - }) - observationUUID, err := sdk.SeededUUID(observationUUIDMap) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - // We've been unable to do much here, but let's try the next one regardless. - continue - } - - // Finding UUID should differ for each individual subject, but remain consistent when validating the same policy for the same subject. - // This acts as an identifier to show the history of a finding. - findingUUIDMap := internal.MergeMaps(subjectAttributeMap, map[string]string{ - "type": "finding", - "policy": result.Policy.Package.PurePackage(), - "policy_file": result.Policy.File, - "policy_path": policyPath, - }) - findingUUID, err := sdk.SeededUUID(findingUUIDMap) - if err != nil { - accumulatedErrors = errors.Join(accumulatedErrors, err) - // We've been unable to do much here, but let's try the next one regardless. - continue - } - - observation := proto.Observation{ - ID: uuid.New().String(), - UUID: observationUUID.String(), - Collected: timestamppb.New(startTime), - Expires: timestamppb.New(startTime.Add(24 * time.Hour)), - Origins: []*proto.Origin{{Actors: actors}}, - Subjects: subjects, - Activities: activities, - Components: components, - RelevantEvidence: []*proto.RelevantEvidence{ - { - Description: fmt.Sprintf("Policy %v was executed against the Local SSH configuration, using the Local SSH Compliance Plugin", result.Policy.Package.PurePackage()), - }, - }, - } - - newFinding := func() *proto.Finding { - controls := make([]*proto.ControlReference, 0) - - for _, control := range result.Controls { - controls = append(controls, &proto.ControlReference{ - Class: control.Class, - ControlId: control.ControlID, - StatementIds: control.StatementIDs, - }) - } - - return &proto.Finding{ - ID: uuid.New().String(), - UUID: findingUUID.String(), - Collected: timestamppb.New(time.Now()), - Labels: map[string]string{ - "type": "ssh", - "host": hostname, - "_policy": result.Policy.Package.PurePackage(), - "_policy_path": result.Policy.File, - }, - Origins: []*proto.Origin{{Actors: actors}}, - Subjects: subjects, - Components: components, - RelatedObservations: []*proto.RelatedObservation{{ObservationUUID: observation.ID}}, - Controls: controls, - } - } - - if len(result.Violations) == 0 { - observation.Title = internal.StringAddressed(fmt.Sprintf("Local SSH Validation on %s passed.", result.Policy.Package.PurePackage())) - observation.Description = fmt.Sprintf("Observed no violations on the %s policy within the Local SSH Compliance Plugin.", result.Policy.Package.PurePackage()) - observations = append(observations, &observation) - - finding := newFinding() - finding.Title = fmt.Sprintf("No violations found on %s", result.Policy.Package.PurePackage()) - finding.Description = fmt.Sprintf("No violations found on the %s policy within the Local SSH Compliance Plugin.", result.Policy.Package.PurePackage()) - finding.Status = &proto.FindingStatus{ - State: runner.FindingTargetStatusSatisfied, - } - findings = append(findings, finding) - continue - } - - if len(result.Violations) > 0 { - observation.Title = internal.StringAddressed(fmt.Sprintf("Validation on %s failed.", result.Policy.Package.PurePackage())) - observation.Description = fmt.Sprintf("Observed %d violation(s) on the %s policy within the Local SSH Compliance Plugin.", len(result.Violations), result.Policy.Package.PurePackage()) - observations = append(observations, &observation) + l.logger.Info("collected observations", "count", len(observations)) + l.logger.Info("collected findings", "count", len(findings)) - for _, violation := range result.Violations { - finding := newFinding() - finding.Title = violation.Title - finding.Description = violation.Description - finding.Remarks = internal.StringAddressed(violation.Remarks) - finding.Status = &proto.FindingStatus{ - State: runner.FindingTargetStatusNotSatisfied, - } - findings = append(findings, finding) - } - } - } - } return observations, findings, accumulatedErrors }