diff --git a/go.mod b/go.mod index 2ec0ff0..e1abfbc 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/config v1.29.9 github.com/aws/aws-sdk-go-v2/service/ec2 v1.208.0 - github.com/compliance-framework/agent v0.0.15 - github.com/compliance-framework/configuration-service v0.0.5 + github.com/compliance-framework/agent v0.1.1 + github.com/compliance-framework/configuration-service v0.1.1 github.com/google/uuid v1.6.0 github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-plugin v1.6.2 @@ -30,7 +30,6 @@ require ( github.com/aws/smithy-go v1.22.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/defenseunicorns/go-oscal v0.6.2 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect diff --git a/go.sum b/go.sum index efd445d..8661f85 100644 --- a/go.sum +++ b/go.sum @@ -56,10 +56,10 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/compliance-framework/agent v0.0.15 h1:VEB3xI3VSgTeudw8L+Czv9VCMt6vM7Nutd+JeMjWjuk= -github.com/compliance-framework/agent v0.0.15/go.mod h1:/ZxHkJJm/wthxm+W7atUgSMfL2217cCaBgN6dfsDSYo= -github.com/compliance-framework/configuration-service v0.0.5 h1:vK9mSb8dzaaTt+hd+g2g3+8nvfr/Ha5pMXB4yiYMA64= -github.com/compliance-framework/configuration-service v0.0.5/go.mod h1:irXS+U+ZGaNrOmaNqb+pMmo+4BxSZJ0/vs4ne/5qVJc= +github.com/compliance-framework/agent v0.1.1 h1:uQ4idgwOMqrgM0JeYCtBv20HZoMymsH2nownrkl457w= +github.com/compliance-framework/agent v0.1.1/go.mod h1:jy/26xgTx9+at64ipTV1oo80pTVyhtlZaSMViQ3cVVQ= +github.com/compliance-framework/configuration-service v0.1.1 h1:p/r5vq1FLe0S8j/kLhth4Dvda8xajVPOBjnO9QauMjM= +github.com/compliance-framework/configuration-service v0.1.1/go.mod h1:tLKJKXbQbY9Pg/e3BJtJVkqxaejXJMHoE8Yp0NW4lDE= github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..feb597f --- /dev/null +++ b/internal/util.go @@ -0,0 +1,15 @@ +package internal + +func MergeMaps(maps ...map[string]string) map[string]string { + result := make(map[string]string) + for _, imap := range maps { + for k, v := range imap { + result[k] = v + } + } + return result +} + +func StringAddressed(str string) *string { + return &str +} diff --git a/main.go b/main.go index 65ddd33..053b53b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/compliance-framework/plugin-aws-ec2/internal" "os" "time" @@ -17,7 +18,6 @@ import ( "github.com/google/uuid" "github.com/hashicorp/go-hclog" goplugin "github.com/hashicorp/go-plugin" - protolang "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -40,13 +40,13 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api ctx := context.TODO() startTime := time.Now() evalStatus := proto.ExecutionStatus_SUCCESS - var errAcc error + var accumulatedErrors error cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(os.Getenv("AWS_REGION"))) if err != nil { l.logger.Error("unable to load SDK config", "error", err) evalStatus = proto.ExecutionStatus_FAILURE - errAcc = errors.Join(errAcc, err) + accumulatedErrors = errors.Join(accumulatedErrors, err) } svc := ec2.NewFromConfig(cfg) @@ -56,7 +56,7 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api if err != nil { l.logger.Error("unable to list instances", "error", err) evalStatus = proto.ExecutionStatus_FAILURE - errAcc = errors.Join(errAcc, err) + accumulatedErrors = errors.Join(accumulatedErrors, err) } // Parse instances @@ -113,177 +113,220 @@ func (l *CompliancePlugin) Eval(request *proto.EvalRequest, apiHelper runner.Api // Run policy checks for _, instance := range instances { + activities := make([]*proto.Activity, 0) + findings := make([]*proto.Finding, 0) + observations := make([]*proto.Observation, 0) + + subjectAttributeMap := map[string]string{ + "type": "aws", + "service": "ec2", + "instance-id": fmt.Sprintf("%v", instance["InstanceID"]), + "image-id": fmt.Sprintf("%v", instance["ImageID"]), + "vpc-id": fmt.Sprintf("%v", instance["VpcID"]), + } + subjects := []*proto.SubjectReference{ + { + Type: "aws-ec2-instance", + Attributes: subjectAttributeMap, + Title: internal.StringAddressed("AWS EC2 Instance"), + Props: []*proto.Property{ + { + Name: "instance-id", + Value: fmt.Sprintf("%v", instance["InstanceID"]), + }, + { + Name: "image-id", + Value: fmt.Sprintf("%v", instance["ImageID"]), + }, + }, + }, + } + 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"), + }, + }, + }, + { + 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"), + }, + }, + }, + } + components := []*proto.ComponentReference{ + { + Identifier: "common-components/aws-ec2", + }, + { + Identifier: "common-components/aws-ec2-instance", + }, + } + for _, policyPath := range request.GetPolicyPaths() { + + 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 Security Group configuration, execute the compiled policies", + }) + results, err := policyManager.New(ctx, l.logger, policyPath).Execute(ctx, "compliance_plugin", instance) if err != nil { l.logger.Error("policy evaluation failed", "error", err) evalStatus = proto.ExecutionStatus_FAILURE - errAcc = errors.Join(errAcc, err) + accumulatedErrors = errors.Join(accumulatedErrors, err) continue } - // Build and send results (this is also from your existing logic) - assessmentResult := runner.NewCallableAssessmentResult() - assessmentResult.Title = "EC2 instance checks - AWS plugin" + activities = append(activities, &proto.Activity{ + Title: "Execute policy", + Description: "Prepare and compile policy bundles, and execute them using the prepared Security Group data", + Steps: steps, + }) for _, result := range results { - // There are no violations reported from the policies. - // We'll send the observation back to the agent - if len(result.Violations) == 0 { - title := "The plugin succeeded. No compliance issues to report." - assessmentResult.AddObservation(&proto.Observation{ - Uuid: uuid.New().String(), - Title: &title, - Description: "The plugin policies did not return any violations. The configuration is in compliance with policies.", - Collected: timestamppb.New(time.Now()), - Expires: timestamppb.New(time.Now().AddDate(0, 1, 0)), // Add one month for the expiration - RelevantEvidence: []*proto.RelevantEvidence{ - { - Description: fmt.Sprintf("Policy %v was evaluated, and no violations were found on machineId: %s", result.Policy.Package.PurePackage(), "ARN:12345"), - }, - }, - Labels: map[string]string{ - "package": string(result.Policy.Package), - "type": "aws", - "service": "ec2", - "instanceID": fmt.Sprintf("%v", instance["InstanceID"]), - }, - }) - - status := runner.FindingTargetStatusSatisfied - assessmentResult.AddFinding(&proto.Finding{ - Title: fmt.Sprintf("No violations found on %s", result.Policy.Package.PurePackage()), - Description: fmt.Sprintf("No violations found on the %s policy within the Template Compliance Plugin.", result.Policy.Package.PurePackage()), - Target: &proto.FindingTarget{ - Status: &proto.ObjectiveStatus{ - State: status, - }, - }, - Labels: map[string]string{ - "package": string(result.Policy.Package), - "type": "aws", - "service": "ec2", - "instanceID": fmt.Sprintf("%v", instance["InstanceID"]), - }, - }) + // 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 } - // There are violations in the policy checks. - // We'll send these observations back to the agent - if len(result.Violations) > 0 { - title := fmt.Sprintf("The plugin found violations for policy %s on machineId: %s", result.Policy.Package.PurePackage(), "ARN:12345") - observationUuid := uuid.New().String() - assessmentResult.AddObservation(&proto.Observation{ - Uuid: observationUuid, - Title: &title, - Description: fmt.Sprintf("Observed %d violation(s) for policy %s", len(result.Violations), result.Policy.Package.PurePackage()), - Collected: timestamppb.New(time.Now()), - Expires: timestamppb.New(time.Now().AddDate(0, 1, 0)), // Add one month for the expiration - RelevantEvidence: []*proto.RelevantEvidence{ - { - Description: fmt.Sprintf("Policy %v was evaluated, and %d violations were found", result.Policy.Package.PurePackage(), len(result.Violations)), - }, + // 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 AWS Security Group configuration, using the Local AWS Security Group Plugin", result.Policy.Package.PurePackage()), }, + }, + } + + newFinding := func() *proto.Finding { + return &proto.Finding{ + ID: uuid.New().String(), + UUID: findingUUID.String(), + Collected: timestamppb.New(time.Now()), Labels: map[string]string{ - "package": string(result.Policy.Package), - "type": "aws", - "service": "ec2", - "instanceID": fmt.Sprintf("%v", instance["InstanceID"]), + "type": "aws", + "service": "ec2", + "instance-id": fmt.Sprintf("%v", instance["InstanceID"]), + "image-id": fmt.Sprintf("%v", instance["ImageID"]), + "vpc-id": fmt.Sprintf("%v", instance["VpcID"]), + "_policy": result.Policy.Package.PurePackage(), + "_policy_path": result.Policy.File, }, - }) - - for _, violation := range result.Violations { - status := runner.FindingTargetStatusNotSatisfied - assessmentResult.AddFinding(&proto.Finding{ - Title: violation.Title, - Description: violation.Description, - Remarks: &violation.Remarks, - RelatedObservations: []*proto.RelatedObservation{ - { - ObservationUuid: observationUuid, - }, - }, - Target: &proto.FindingTarget{ - Status: &proto.ObjectiveStatus{ - State: status, - }, - }, - Labels: map[string]string{ - "package": string(result.Policy.Package), - "type": "aws", - "service": "ec2", - "instanceID": fmt.Sprintf("%v", instance["InstanceID"]), - }, - }) + Origins: []*proto.Origin{{Actors: actors}}, + Subjects: subjects, + Components: components, + RelatedObservations: []*proto.RelatedObservation{{ObservationUUID: observation.ID}}, + Controls: nil, } } - for _, risk := range result.Risks { - links := []*proto.Link{} - for _, link := range risk.Links { - links = append(links, &proto.Link{ - Href: link.URL, - Text: &link.Text, - }) - } + // There are no violations reported from the policies. + // We'll send the observation back to the agent + if len(result.Violations) == 0 { - assessmentResult.AddRiskEntry(&proto.Risk{ - Title: risk.Title, - Description: risk.Description, - Statement: risk.Statement, - Props: []*proto.Property{}, - Links: links, - }) - } - } + observation.Title = internal.StringAddressed("The plugin succeeded. No compliance issues to report.") + observation.Description = "The plugin policies did not return any violations. The configuration is in compliance with policies." + observations = append(observations, &observation) - assessmentResult.Start = timestamppb.New(startTime) + finding := newFinding() + finding.Title = fmt.Sprintf("No violations found on %s", result.Policy.Package.PurePackage()) + finding.Description = fmt.Sprintf("No violations were found on the %s policy within the AWS EC2 Compliance Plugin.", result.Policy.Package.PurePackage()) + finding.Status = &proto.FindingStatus{ + State: runner.FindingTargetStatusSatisfied, + } + findings = append(findings, finding) + continue + } - var endTime = time.Now() - assessmentResult.End = timestamppb.New(endTime) + // There are violations in the policy checks. + // We'll send these observations back to the agent + 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 AWS EC2 Compliance Plugin.", len(result.Violations), result.Policy.Package.PurePackage()) + observations = append(observations, &observation) - streamId, err := sdk.SeededUUID(map[string]string{ - "type": "aws", - "service": "ec2", - "_policy": policyPath, - "instance_id": fmt.Sprintf("%v", instance["InstanceID"]), - }) - if err != nil { - l.logger.Error("Failed to seedUUID", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - errAcc = errors.Join(errAcc, err) - continue + 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) + } + } } + } - assessmentResult.AddLogEntry(&proto.AssessmentLog_Entry{ - Title: protolang.String("Template check"), - Description: protolang.String("Template plugin checks completed successfully"), - Start: timestamppb.New(startTime), - End: timestamppb.New(endTime), - }) + if err = apiHelper.CreateObservations(ctx, observations); err != nil { + l.logger.Error("Failed to send observations", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err + } - err = apiHelper.CreateResult( - streamId.String(), - map[string]string{ - "type": "aws", - "service": "ec2", - "instance_id": fmt.Sprintf("%v", instance["InstanceID"]), - "_policy": policyPath, - }, - policyPath, - assessmentResult.Result()) - if err != nil { - l.logger.Error("Failed to add assessment result", "error", err) - evalStatus = proto.ExecutionStatus_FAILURE - errAcc = errors.Join(errAcc, err) - } + if err = apiHelper.CreateFindings(ctx, findings); err != nil { + l.logger.Error("Failed to send findings", "error", err) + return &proto.EvalResponse{ + Status: proto.ExecutionStatus_FAILURE, + }, err } } return &proto.EvalResponse{ Status: evalStatus, - }, errAcc + }, accumulatedErrors } func main() {