diff --git a/internal/datalakes/inmemory/policyhub.go b/internal/datalakes/inmemory/policyhub.go index 8dd79cad..46f927e6 100644 --- a/internal/datalakes/inmemory/policyhub.go +++ b/internal/datalakes/inmemory/policyhub.go @@ -522,7 +522,7 @@ func (db *Db) entityGraphExecutionChecksum(ctx context.Context, mrn string) (str } } - return policy.BundleExecutionChecksum(policyObj, framework), nil + return policy.BundleExecutionChecksum(ctx, policyObj, framework), nil } // EntityGraphContentChecksum retrieves the content checksum for a given entity. diff --git a/policy/bundle.go b/policy/bundle.go index b6778aac..ccf2c6d4 100644 --- a/policy/bundle.go +++ b/policy/bundle.go @@ -91,7 +91,7 @@ func (l *BundleLoader) BundleFromPaths(paths ...string) (*Bundle, error) { // BundleExecutionChecksum creates a combined execution checksum from a policy // and framework. Either may be nil. -func BundleExecutionChecksum(policy *Policy, framework *Framework) string { +func BundleExecutionChecksum(ctx context.Context, policy *Policy, framework *Framework) string { res := checksums.New if policy != nil { res = res.Add(policy.GraphExecutionChecksum) @@ -102,7 +102,11 @@ func BundleExecutionChecksum(policy *Policy, framework *Framework) string { // So far the checksum only includes the policy and the framework // It does not change if any of the jobs changes, only if the policy or the framework changes // To update the resolved policy, when we change how it is generated, change the incoporated version of the resolver - res = res.Add(RESOLVER_VERSION) + if IsNextGenResolver(ctx) { + res = res.Add(RESOLVER_VERSION_NG) + } else { + res = res.Add(RESOLVER_VERSION) + } return res.String() } diff --git a/policy/cnspec_policy.pb.go b/policy/cnspec_policy.pb.go index 6ee12a6a..dc59039c 100644 --- a/policy/cnspec_policy.pb.go +++ b/policy/cnspec_policy.pb.go @@ -493,33 +493,36 @@ type ReportingJob_Type int32 const ( ReportingJob_UNSPECIFIED ReportingJob_Type = 0 - ReportingJob_CHECK ReportingJob_Type = 1 - ReportingJob_DATA_QUERY ReportingJob_Type = 2 ReportingJob_CONTROL ReportingJob_Type = 3 ReportingJob_POLICY ReportingJob_Type = 4 ReportingJob_FRAMEWORK ReportingJob_Type = 5 ReportingJob_RISK_FACTOR ReportingJob_Type = 6 + // DO NOT USE CHECK OR DATA_QUERY, THEY ARE DEPRECATED + // Here's the reason why: + // A query can be either or both. We cannot pick one in all cases + ReportingJob_CHECK ReportingJob_Type = 1 + ReportingJob_DATA_QUERY ReportingJob_Type = 2 ) // Enum value maps for ReportingJob_Type. var ( ReportingJob_Type_name = map[int32]string{ 0: "UNSPECIFIED", - 1: "CHECK", - 2: "DATA_QUERY", 3: "CONTROL", 4: "POLICY", 5: "FRAMEWORK", 6: "RISK_FACTOR", + 1: "CHECK", + 2: "DATA_QUERY", } ReportingJob_Type_value = map[string]int32{ "UNSPECIFIED": 0, - "CHECK": 1, - "DATA_QUERY": 2, "CONTROL": 3, "POLICY": 4, "FRAMEWORK": 5, "RISK_FACTOR": 6, + "CHECK": 1, + "DATA_QUERY": 2, } ) @@ -7078,12 +7081,12 @@ var file_cnspec_policy_proto_rawDesc = []byte{ 0x63, 0x6e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x2e, 0x65, 0x78, 0x70, 0x6c, 0x6f, 0x72, 0x65, 0x72, 0x2e, 0x49, 0x6d, 0x70, 0x61, 0x63, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, - 0x48, 0x45, 0x43, 0x4b, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x51, - 0x55, 0x45, 0x52, 0x59, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x4e, 0x54, 0x52, 0x4f, - 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x10, 0x04, 0x12, - 0x0d, 0x0a, 0x09, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x57, 0x4f, 0x52, 0x4b, 0x10, 0x05, 0x12, 0x0f, - 0x0a, 0x0b, 0x52, 0x49, 0x53, 0x4b, 0x5f, 0x46, 0x41, 0x43, 0x54, 0x4f, 0x52, 0x10, 0x06, 0x22, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, + 0x4f, 0x4e, 0x54, 0x52, 0x4f, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x4f, 0x4c, 0x49, + 0x43, 0x59, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x52, 0x41, 0x4d, 0x45, 0x57, 0x4f, 0x52, + 0x4b, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x52, 0x49, 0x53, 0x4b, 0x5f, 0x46, 0x41, 0x43, 0x54, + 0x4f, 0x52, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x01, 0x12, + 0x0e, 0x0a, 0x0a, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x59, 0x10, 0x02, 0x22, 0xcc, 0x07, 0x0a, 0x06, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x72, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x63, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x72, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65, diff --git a/policy/cnspec_policy.proto b/policy/cnspec_policy.proto index 6d94d4a5..b8623acc 100644 --- a/policy/cnspec_policy.proto +++ b/policy/cnspec_policy.proto @@ -556,12 +556,16 @@ message ReportingJob { enum Type { UNSPECIFIED = 0; - CHECK = 1; - DATA_QUERY = 2; CONTROL = 3; POLICY = 4; FRAMEWORK = 5; RISK_FACTOR = 6; + + // DO NOT USE CHECK OR DATA_QUERY, THEY ARE DEPRECATED + // Here's the reason why: + // A query can be either or both. We cannot pick one in all cases + CHECK = 1; + DATA_QUERY = 2; } string checksum = 1; diff --git a/policy/resolved_policy_builder.go b/policy/resolved_policy_builder.go new file mode 100644 index 00000000..87239dee --- /dev/null +++ b/policy/resolved_policy_builder.go @@ -0,0 +1,1151 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package policy + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "go.mondoo.com/cnquery/v11/explorer" + "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/mqlc" + "go.mondoo.com/cnquery/v11/mrn" +) + +// buildResolvedPolicy builds a resolved policy from a bundle +func buildResolvedPolicy(ctx context.Context, bundleMrn string, bundle *Bundle, assetFilters []*explorer.Mquery, now time.Time, compilerConf mqlc.CompilerConfig) (*ResolvedPolicy, error) { + bundleMap := bundle.ToMap() + assetFilterMap := make(map[string]struct{}, len(assetFilters)) + for _, f := range assetFilters { + assetFilterMap[f.CodeId] = struct{}{} + } + + policyObj := bundleMap.Policies[bundleMrn] + frameworkObj := bundleMap.Frameworks[bundleMrn] + + builder := &resolvedPolicyBuilder{ + bundleMrn: bundleMrn, + bundleMap: bundleMap, + assetFilters: assetFilterMap, + nodes: map[string]rpBuilderNode{}, + reportsToEdges: map[string][]string{}, + reportsFromEdges: map[string][]edgeImpact{}, + policyScoringSystems: map[string]explorer.ScoringSystem{}, + actionOverrides: map[string]explorer.Action{}, + impactOverrides: map[string]*explorer.Impact{}, + riskMagnitudes: map[string]*RiskMagnitude{}, + propsCache: explorer.NewPropsCache(), + queryTypes: map[string]queryType{}, + now: now, + } + + builder.gatherGlobalInfoFromPolicy(policyObj) + builder.gatherGlobalInfoFromFramework(frameworkObj) + builder.collectQueryTypes(bundleMrn, builder.queryTypes) + + builder.addPolicy(policyObj) + + if frameworkObj != nil { + builder.addFramework(frameworkObj) + } + + resolvedPolicyExecutionChecksum := BundleExecutionChecksum(ctx, policyObj, frameworkObj) + assetFiltersChecksum, err := ChecksumAssetFilters(assetFilters, compilerConf) + if err != nil { + return nil, err + } + + builderData := &rpBuilderData{ + baseChecksum: checksumStrings(resolvedPolicyExecutionChecksum, assetFiltersChecksum, "v2"), + propsCache: builder.propsCache, + compilerConf: compilerConf, + } + + resolvedPolicy := &ResolvedPolicy{ + ExecutionJob: &ExecutionJob{ + Checksum: "", + Queries: map[string]*ExecutionQuery{}, + }, + CollectorJob: &CollectorJob{ + Checksum: "", + ReportingJobs: map[string]*ReportingJob{}, + ReportingQueries: map[string]*StringArray{}, + Datapoints: map[string]*DataQueryInfo{}, + RiskMrns: map[string]*StringArray{}, + RiskFactors: map[string]*RiskFactor{}, + }, + Filters: assetFilters, + GraphExecutionChecksum: resolvedPolicyExecutionChecksum, + FiltersChecksum: assetFiltersChecksum, + } + + // We will walk the graph from the non prunable nodes out. This means that if something is not connected + // to a non prunable node, it will not be included in the resolved policy + nonPrunables := make([]rpBuilderNode, 0, len(builder.nodes)) + + for _, n := range builder.nodes { + if !n.isPrunable() { + nonPrunables = append(nonPrunables, n) + } + } + + visited := make(map[string]struct{}, len(builder.nodes)) + var walk func(node rpBuilderNode) error + walk = func(node rpBuilderNode) error { + // Check if we've already visited this node + if _, ok := visited[node.getId()]; ok { + return nil + } + visited[node.getId()] = struct{}{} + + // Build the necessary parts of the resolved policy for each node + if err := node.build(resolvedPolicy, builderData); err != nil { + log.Error().Err(err).Str("node", node.getId()).Msg("error building node") + return err + } + // Walk to each parent node and recurse + for _, edge := range builder.reportsToEdges[node.getId()] { + if edgeNode, ok := builder.nodes[edge]; ok { + if err := walk(edgeNode); err != nil { + return err + } + + } else { + log.Debug().Str("from", node.getId()).Str("to", edge).Msg("edge not found") + } + } + return nil + } + + for _, n := range nonPrunables { + if err := walk(n); err != nil { + return nil, err + } + } + + // We need to connect the reporting jobs. We've stored them by uuid in the collector job. However, + // our graph uses the qr id to connect them. + reportingJobsByQrId := make(map[string]*ReportingJob, len(resolvedPolicy.CollectorJob.ReportingJobs)) + for _, rj := range resolvedPolicy.CollectorJob.ReportingJobs { + if _, ok := reportingJobsByQrId[rj.QrId]; ok { + // We should never have multiple reporting jobs with the same qr id. Scores are stored + // by qr id, not by uuid. This would cause issues where scores would flop around + log.Error().Str("qr_id", rj.QrId).Msg("multipe reporting jobs with the same qr id") + return nil, errors.New("multiple reporting jobs with the same qr id") + } + reportingJobsByQrId[rj.QrId] = rj + } + + // For each parent qr id, we need to connect the child reporting jobs with the impact. + // connectReportingJobNotifies will add the link from the child to the parent, and + // the parent to the child with the impact + for parentQrId, edges := range builder.reportsFromEdges { + for _, edge := range edges { + parent := reportingJobsByQrId[parentQrId] + if parent == nil { + // It's possible that the parent reporting job was not included in the resolved policy + // because it was not connected to a leaf node (e.g. a policy that was not connected to + // any check or data query). In this case, we can just skip it + log.Debug().Str("parent", parentQrId).Msg("reporting job not found") + continue + } + + if child, ok := reportingJobsByQrId[edge.edge]; ok { + // Also possible a child was not included in the resolved policy + connectReportingJobNotifies(child, parent, edge.impact) + } + } + } + + rootReportingJob := reportingJobsByQrId[bundleMrn] + if rootReportingJob == nil { + return nil, explorer.NewAssetMatchError(bundleMrn, "policies", "no-matching-policy", assetFilters, policyObj.ComputedFilters) + } + rootReportingJob.QrId = "root" + + resolvedPolicy.ReportingJobUuid = rootReportingJob.Uuid + + refreshChecksums(resolvedPolicy.ExecutionJob, resolvedPolicy.CollectorJob) + for _, rj := range resolvedPolicy.CollectorJob.ReportingJobs { + rj.RefreshChecksum() + } + + return resolvedPolicy, nil +} + +// resolvedPolicyBuilder contains data that helps build the resolved policy. It maintains a graph of nodes. +// These nodes are the policies, controls, frameworks, checks, data queries, and execution queries. They +// get a chance to add themselves to the resolved policy in the way that they need to be added. They all +// add reporting jobs. Some nodes do other things like add the compiled query to the resolved policy. These nodes +// are connected by edges. These edges are the edges used to connect the reporting jobs in the resolved policy. +// Edges are added using the addEdge method. This will take care of maintaining the notifies edge and the childJobs +// edge from the reporting jobs simultaneously so that they are in sync. +type resolvedPolicyBuilder struct { + // bundleMrn is the mrn of the bundle that is being resolved. It will be replaced by "root" in the + // resolved policy's reporting jobs so that it can be reused by other bundles that are identical in + // everything except the mrn of the root. + bundleMrn string + // bundleMap is the bundle that is being resolved converted into a PolicyBundleMap + bundleMap *PolicyBundleMap + + // nodes is a map of all the nodes that are in the graph. These nodes will build the resolved + // policy. nodes is walked from the non prunable nodes out. This means that if something is not + // connected to a non prunable node, it will not be included in the resolved policy + nodes map[string]rpBuilderNode + // reportsToEdges maintains the notifies edges from the reporting jobs. + reportsToEdges map[string][]string + // reportsFromEdges maintains the childJobs edges from the reporting jobs. This is where the impact + // is stored. + reportsFromEdges map[string][]edgeImpact + + // assetFilters is the asset filters that are used to select the policies and queries that are + // run + assetFilters map[string]struct{} + // policyScoringSystems is a map of the scoring systems for each policy + policyScoringSystems map[string]explorer.ScoringSystem + // actionOverrides is a map of the actions that are overridden by the policies + actionOverrides map[string]explorer.Action + // impactOverrides is a map of the impacts that are overridden by the policies. The worst impact + // is used + impactOverrides map[string]*explorer.Impact + // riskMagnitudes is a map of the risk magnitudes that are set for risk factors + riskMagnitudes map[string]*RiskMagnitude + // queryTypes is a map of the query types for each query. A query can be a scoring query, a data query, + // or both. We analyze all matching policies to determine the query type. If a query shows up in checks, + // it is a scoring query. If it shows up in data queries, it is a data query. If it shows up in both, it is + // set to both. + queryTypes map[string]queryType + // propsCache is a cache of the properties that are used in the queries + propsCache explorer.PropsCache + // now is the time that the resolved policy is being built + now time.Time +} + +type edgeImpact struct { + edge string + impact *explorer.Impact +} + +// rpBuilderNode is a node in the graph. It represents a policy, control, framework, check, data query, or execution query. +// Each node implementation decides how it needs to be added to the resolved policy. It is currently assumed that +// each node will add a reporting job to the resolved policy, as the edges are used to automatically connect the reporting jobs. +type rpBuilderNode interface { + // getId returns the id of the node. This is used to identify the node in the graph, a mrn or code id + getId() string + // isPrunable returns whether the node can be pruned from the graph. It will be pruned if it a non-prunable node + // doesn't have a path TO it. In context of building the resolved policy, this means that the node is not connected + // to an executable query, or is the root node. + isPrunable() bool + // build is responsible for updating the resolved policy. It will add things like reporting jobs, connect datapoints, + // adding the compiled query, etc. + build(*ResolvedPolicy, *rpBuilderData) error +} + +// rpBuilderData is the data that is used to build the resolved policy +type rpBuilderData struct { + baseChecksum string + propsCache explorer.PropsCache + compilerConf mqlc.CompilerConfig +} + +func (d *rpBuilderData) relativeChecksum(s string) string { + return checksumStrings(d.baseChecksum, s) +} + +// rpBuilderPolicyNode is a node that represents a policy in the graph. It will add a reporting job to the resolved policy +// for the policy +type rpBuilderPolicyNode struct { + policy *Policy + scoringSystem explorer.ScoringSystem + isRoot bool +} + +func (n *rpBuilderPolicyNode) getId() string { + return n.policy.Mrn +} + +func (n *rpBuilderPolicyNode) isPrunable() bool { + // We do not allow pruning the root node. This covers cases where the policy matches the asset filters, + // but we have no active checks or queries. This will end up reporting a U for the score + return !n.isRoot +} + +func (n *rpBuilderPolicyNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + if n.isRoot { + // If the policy is the root, we need a different checksum for the reporting job because we want it + // to be reusable by other bundles that are identical in everything except the root mrn + addReportingJob(n.policy.Mrn, true, data.relativeChecksum(n.policy.GraphExecutionChecksum), ReportingJob_POLICY, rp) + } else { + // the uuid used to be a checksum of the policy mrn, impact, and action + // I don't think this can be correct in all cases as you could at some point + // have a policy report to multiple other policies with different impacts + // (we don't have that case right now) + // These checksum changes should be accounted for in the root + rj := addReportingJob(n.policy.Mrn, true, data.relativeChecksum(n.policy.Mrn), ReportingJob_POLICY, rp) + rj.ScoringSystem = n.scoringSystem + } + + return nil +} + +// rpBuilderGenericQueryNode is a node that represents a query by mrn in the graph. It will add a reporting job, +// and fill out the reporting queries in the resolved policy +type rpBuilderGenericQueryNode struct { + // queryMrn is the mrn of the query + queryMrn string + // queryType is the type of query. It can be a scoring query, a data query, or both + queryType queryType + // selectedCodeId is the code id that actually gets executed. It is the code id of the specific query + // that is run, traversed down the variants if necessary. We keep track of this because we need to connect + // controls to the specific query that is run so they are not influenced by impacts + selectedCodeId string +} + +func (n *rpBuilderGenericQueryNode) getId() string { + return n.queryMrn +} + +func (n *rpBuilderGenericQueryNode) isPrunable() bool { + return true +} + +func (n *rpBuilderGenericQueryNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + reportingJobUUID := data.relativeChecksum(n.queryMrn) + + // Because a query can be both a scoring query and a data query, UNSPECIFIED is used + // for the reporting job type. We need to get rid of the specific types for check and + // data query and have something that can be both + addReportingJob(n.queryMrn, true, reportingJobUUID, ReportingJob_UNSPECIFIED, rp) + + // Add scoring queries to the reporting queries section + if n.queryType == queryTypeScoring || n.queryType == queryTypeBoth { + codeIdReportingJobUUID := data.relativeChecksum(n.selectedCodeId) + + if _, ok := rp.CollectorJob.ReportingQueries[n.selectedCodeId]; !ok { + rp.CollectorJob.ReportingQueries[n.selectedCodeId] = &StringArray{} + } + + // Add the reporting job to the reporting queries if it does not already exist + if !slices.Contains(rp.CollectorJob.ReportingQueries[n.selectedCodeId].Items, codeIdReportingJobUUID) { + rp.CollectorJob.ReportingQueries[n.selectedCodeId].Items = append(rp.CollectorJob.ReportingQueries[n.selectedCodeId].Items, codeIdReportingJobUUID) + } + } + + return nil +} + +// rpBuilderExecutionQueryNode is a node that represents a executable query in the graph. It will add a reporting job to the resolved policy, +// and add the compiled query to the execution job, and connect the datapoints to the reporting job. +// This node is a leaf. Anything connected to an executable query will not be pruned. +// This node is represented by a code id in the reporting jobs. We do not apply impact at this point so +// any scores will be either 0 or 100 +type rpBuilderExecutionQueryNode struct { + query *explorer.Mquery +} + +func (n *rpBuilderExecutionQueryNode) getId() string { + return n.query.CodeId +} + +func (n *rpBuilderExecutionQueryNode) isPrunable() bool { + // Executable queries are leaf nodes in the graph. They cannot be pruned + // If something is connected to an executable query, we want to keep it around + return false +} + +func (n *rpBuilderExecutionQueryNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + // Compile the properties + propTypes, propToChecksums, err := compileProps(n.query, rp, data) + if err != nil { + return err + } + // Add the compiled query to the execution job. This also collects the datapoints into the collector job + executionQuery, _, err := mquery2executionQuery(n.query, propTypes, propToChecksums, rp.CollectorJob, false, data.compilerConf) + if err != nil { + return err + } + rp.ExecutionJob.Queries[n.query.CodeId] = executionQuery + + codeIdReportingJobUUID := data.relativeChecksum(n.query.CodeId) + + // Create a reporting job for the code id + codeIdReportingJob := addReportingJob(n.query.CodeId, false, codeIdReportingJobUUID, ReportingJob_UNSPECIFIED, rp) + // Connect the datapoints to the reporting job + err = connectDatapointsToReportingJob(executionQuery, codeIdReportingJob, rp.CollectorJob.Datapoints) + if err != nil { + return err + } + + return nil +} + +// rpBuilderFrameworkNode is a node that represents a framework in the graph. It will add a reporting job to the resolved policy +type rpBuilderFrameworkNode struct { + frameworkMrn string +} + +func (n *rpBuilderFrameworkNode) getId() string { + return n.frameworkMrn +} + +func (n *rpBuilderFrameworkNode) isPrunable() bool { + return true +} + +func (n *rpBuilderFrameworkNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + addReportingJob(n.frameworkMrn, true, data.relativeChecksum(n.frameworkMrn), ReportingJob_FRAMEWORK, rp) + return nil +} + +// rpBuilderControlNode is a node that represents a control in the graph. It will add a reporting job to the resolved policy +type rpBuilderControlNode struct { + controlMrn string +} + +func (n *rpBuilderControlNode) getId() string { + return n.controlMrn +} + +func (n *rpBuilderControlNode) isPrunable() bool { + return true +} + +func (n *rpBuilderControlNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + addReportingJob(n.controlMrn, true, data.relativeChecksum(n.controlMrn), ReportingJob_CONTROL, rp) + return nil +} + +// rpBuilderRiskFactorNode is a node that represents a risk factor in the graph. It will add a reporting job to the resolved policy, +// and fill out the RiskFactors and RiskMrns sections in the collector job +type rpBuilderRiskFactorNode struct { + riskFactor *RiskFactor + magnitude *RiskMagnitude + selectedCodeIds []string +} + +func (n *rpBuilderRiskFactorNode) getId() string { + return n.riskFactor.Mrn +} + +func (n *rpBuilderRiskFactorNode) isPrunable() bool { + return true +} + +func (n *rpBuilderRiskFactorNode) build(rp *ResolvedPolicy, data *rpBuilderData) error { + risk := n.riskFactor + if n.magnitude != nil { + risk.Magnitude = n.magnitude + } + rp.CollectorJob.RiskFactors[risk.Mrn] = &RiskFactor{ + Scope: risk.Scope, + Magnitude: risk.Magnitude, + Resources: risk.Resources, + DeprecatedV11Magnitude: risk.Magnitude.GetValue(), + DeprecatedV11IsAbsolute: risk.Magnitude.GetIsToxic(), + } + reportingJobUUID := data.relativeChecksum(risk.Mrn) + addReportingJob(risk.Mrn, true, reportingJobUUID, ReportingJob_RISK_FACTOR, rp) + + for _, codeId := range n.selectedCodeIds { + uuid := data.relativeChecksum(codeId) + if _, ok := rp.CollectorJob.RiskMrns[uuid]; !ok { + rp.CollectorJob.RiskMrns[uuid] = &StringArray{ + Items: []string{}, + } + } + rp.CollectorJob.RiskMrns[uuid].Items = append(rp.CollectorJob.RiskMrns[uuid].Items, risk.Mrn) + } + return nil +} + +func (b *resolvedPolicyBuilder) addEdge(from, to string, impact *explorer.Impact) { + if _, ok := b.reportsToEdges[from]; !ok { + b.reportsToEdges[from] = make([]string, 0, 1) + } + for _, e := range b.reportsToEdges[from] { + // If the edge already exists, don't add it + if e == to { + return + } + } + b.reportsToEdges[from] = append(b.reportsToEdges[from], to) + + if _, ok := b.reportsFromEdges[to]; !ok { + b.reportsFromEdges[to] = make([]edgeImpact, 0, 1) + } + + b.reportsFromEdges[to] = append(b.reportsFromEdges[to], edgeImpact{edge: from, impact: impact}) +} + +func (b *resolvedPolicyBuilder) addNode(node rpBuilderNode) { + b.nodes[node.getId()] = node +} + +type queryType int + +const ( + queryTypeScoring queryType = iota + queryTypeData + queryTypeBoth +) + +// collectQueryTypes collects the query types for each query in the policy. A query can be a scoring query, a data query, +// or both. We analyze all matching policies to determine the query type. If a query shows up in checks, it is a scoring query. +// If it shows up in data queries, it is a data query. If it shows up in both, it is set to both. +func (b *resolvedPolicyBuilder) collectQueryTypes(policyMrn string, acc map[string]queryType) { + policy := b.bundleMap.Policies[policyMrn] + if policy == nil { + return + } + + var accumulate func(queryMrn string, t queryType) + accumulate = func(queryMrn string, t queryType) { + if existing, ok := acc[queryMrn]; !ok { + // If it doesn't exist, add it + acc[queryMrn] = t + } else { + if existing != t && existing != queryTypeBoth { + // If it exists, but is different, set it to both + acc[queryMrn] = queryTypeBoth + } + } + q := b.bundleMap.Queries[queryMrn] + if q == nil { + return + } + + for _, v := range q.Variants { + accumulate(v.Mrn, t) + } + } + + for _, g := range policy.Groups { + if !b.isGroupMatching(g) { + // skip groups that don't match + continue + } + + for _, c := range g.Checks { + accumulate(c.Mrn, queryTypeScoring) + } + + for _, q := range g.Queries { + accumulate(q.Mrn, queryTypeData) + } + + for _, pRef := range g.Policies { + // recursively collect query types from referenced policies + b.collectQueryTypes(pRef.Mrn, acc) + } + } + + // queries in risk factors are checks + for _, r := range policy.RiskFactors { + for _, c := range r.Checks { + accumulate(c.Mrn, queryTypeScoring) + } + } +} + +func (b *resolvedPolicyBuilder) gatherGlobalInfoFromFramework(framework *Framework) { + actions := b.actionOverrides + + if framework == nil { + return + } + + for _, fRef := range framework.Dependencies { + f := b.bundleMap.Frameworks[fRef.Mrn] + if f == nil { + continue + } + b.gatherGlobalInfoFromFramework(f) + } + + for _, g := range framework.Groups { + if !b.isGroupMatching(g) { + continue + } + + for _, c := range g.Controls { + action := normalizeAction(g.Type, c.Action, nil) + if action != explorer.Action_UNSPECIFIED && action != explorer.Action_MODIFY { + actions[c.Mrn] = action + } + } + } +} + +// gatherGlobalInfoFromPolicy gathers the action, impact, scoring system, and risk magnitude overrides from the policy. We +// apply this information in a second pass when building the nodes +func (b *resolvedPolicyBuilder) gatherGlobalInfoFromPolicy(policy *Policy) { + actions := b.actionOverrides + impacts := b.impactOverrides + scoringSystems := b.policyScoringSystems + riskMagnitudes := b.riskMagnitudes + + for _, g := range policy.Groups { + if !b.isGroupMatching(g) { + continue + } + for _, pRef := range g.Policies { + p := b.bundleMap.Policies[pRef.Mrn] + + b.gatherGlobalInfoFromPolicy(p) + + action := normalizeAction(g.Type, pRef.Action, pRef.Impact) + if action != explorer.Action_UNSPECIFIED && action != explorer.Action_MODIFY { + actions[pRef.Mrn] = action + } + + if pRef.Impact != nil { + impacts[pRef.Mrn] = pRef.Impact + } + scoringSystem := pRef.ScoringSystem + + if scoringSystem != explorer.ScoringSystem_SCORING_UNSPECIFIED { + scoringSystems[pRef.Mrn] = pRef.ScoringSystem + } else { + if p, ok := b.bundleMap.Policies[pRef.Mrn]; ok { + scoringSystems[pRef.Mrn] = p.ScoringSystem + } + } + } + + // We always want to select the worst impact that we find + getWorstImpact := func(impact1 *explorer.Impact, impact2 *explorer.Impact) *explorer.Impact { + if impact1 == nil { + return impact2 + } + if impact2 == nil { + return impact1 + } + + if impact1.Value.GetValue() > impact2.Value.GetValue() { + return impact1 + } + return impact2 + } + + for _, c := range g.Checks { + impact := c.Impact + if qBundle, ok := b.bundleMap.Queries[c.Mrn]; ok { + // Check the impact defined on the query + impact = getWorstImpact(impact, qBundle.Impact) + } + + impact = getWorstImpact(impact, impacts[c.Mrn]) + + action := normalizeAction(g.Type, c.Action, impact) + if action != explorer.Action_UNSPECIFIED && action != explorer.Action_MODIFY { + actions[c.Mrn] = action + } + + if impact != nil { + impacts[c.Mrn] = impact + } + } + + for _, q := range g.Queries { + if q.Action != explorer.Action_UNSPECIFIED { + action := normalizeAction(g.Type, q.Action, q.Impact) + if action != explorer.Action_UNSPECIFIED && action != explorer.Action_MODIFY { + actions[q.Mrn] = action + } + } + } + } + + for _, r := range policy.RiskFactors { + if r.Magnitude != nil { + riskMagnitudes[r.Mrn] = r.Magnitude + } + + if r.Action != explorer.Action_UNSPECIFIED && r.Action != explorer.Action_MODIFY { + actions[r.Mrn] = r.Action + } + } +} + +func canRun(action explorer.Action) bool { + return !(action == explorer.Action_DEACTIVATE || action == explorer.Action_OUT_OF_SCOPE) +} + +type group interface { + GetReviewStatus() ReviewStatus + GetEndDate() int64 +} + +type groupWithFilters interface { + group + GetFilters() *explorer.Filters +} + +// isGroupMatching checks if the policy group is matching. A policy group is matching if it is not rejected, +// and it is not expired. If it has filters, it must have at least one filter that matches the asset filters +func (b *resolvedPolicyBuilder) isGroupMatching(group group) bool { + if group.GetReviewStatus() == ReviewStatus_REJECTED { + return false + } + + if group.GetEndDate() != 0 { + endDate := time.Unix(group.GetEndDate(), 0) + if endDate.Before(b.now) { + return false + } + } + + if groupWithFilters, ok := group.(groupWithFilters); ok { + if groupWithFilters.GetFilters() == nil || len(groupWithFilters.GetFilters().Items) == 0 { + return true + } + + for _, filter := range groupWithFilters.GetFilters().Items { + if _, ok := b.assetFilters[filter.CodeId]; ok { + return true + } + } + } else { + return true + } + + return false +} + +// addPolicy recurses a policy and adds all the nodes and edges to the graph. It will add the policy, its dependent policies, checks, and queries +func (b *resolvedPolicyBuilder) addPolicy(policy *Policy) bool { + action := b.actionOverrides[policy.Mrn] + + // Check if we can run this policy. If not, then we do not add it to the graph + if !canRun(action) { + return false + } + + if !b.anyFilterMatches(policy.ComputedFilters) { + return false + } + + b.propsCache.Add(policy.Props...) + + // Add node for policy + scoringSystem := b.policyScoringSystems[policy.Mrn] + b.addNode(&rpBuilderPolicyNode{policy: policy, scoringSystem: scoringSystem, isRoot: b.bundleMrn == policy.Mrn}) + hasMatchingGroup := false + for _, g := range policy.Groups { + if !b.isGroupMatching(g) { + continue + } + hasMatchingGroup = true + for _, pRef := range g.Policies { + p := b.bundleMap.Policies[pRef.Mrn] + if b.addPolicy(p) { + var impact *explorer.Impact + if pRefAction, ok := b.actionOverrides[pRef.Mrn]; ok && pRefAction == explorer.Action_IGNORE { + impact = &explorer.Impact{ + Scoring: explorer.ScoringSystem_IGNORE_SCORE, + } + } else if i, ok := b.impactOverrides[pRef.Mrn]; ok { + impact = i + } + b.addEdge(pRef.Mrn, policy.Mrn, impact) + } + } + + for _, c := range g.Checks { + // Check the action. If its an override, we don't need to add the check + // because it will get included in a policy that wants it run. + // This will prevent the check from being connected to the policy that + // overrides its action + if isOverride(c.Action, g.Type) { + b.propsCache.Add(c.Props...) + continue + } + + c, ok := b.bundleMap.Queries[c.Mrn] + if !ok { + log.Warn().Str("mrn", c.Mrn).Msg("check not found in bundle") + continue + } + + if _, ok := b.addQuery(c); ok { + action := b.actionOverrides[c.Mrn] + var impact *explorer.Impact + if action == explorer.Action_IGNORE { + impact = &explorer.Impact{ + Scoring: explorer.ScoringSystem_IGNORE_SCORE, + } + } + b.addEdge(c.Mrn, policy.Mrn, impact) + } + } + + for _, q := range g.Queries { + // Check the action. If its an override, we don't need to add the query + // because it will get included in a policy that wants it run. + // This will prevent the query from being connected to the policy that + // overrides its action + if isOverride(q.Action, g.Type) { + b.propsCache.Add(q.Props...) + continue + } + + q, ok := b.bundleMap.Queries[q.Mrn] + if !ok { + log.Warn().Str("mrn", q.Mrn).Msg("query not found in bundle") + continue + } + + if _, ok := b.addQuery(q); ok { + b.addEdge(q.Mrn, policy.Mrn, &explorer.Impact{ + Scoring: explorer.ScoringSystem_IGNORE_SCORE, + }) + } + } + } + + hasMatchingRiskFactor := false + for _, r := range policy.RiskFactors { + if len(r.Checks) == 0 || isOverride(r.Action, GroupType_UNCATEGORIZED) { + continue + } + + added, err := b.addRiskFactor(r) + if err != nil { + log.Error().Err(err).Str("mrn", r.Mrn).Msg("error adding risk factor") + continue + } + if added { + b.addEdge(r.Mrn, policy.Mrn, &explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + hasMatchingRiskFactor = true + } + } + + return hasMatchingGroup || hasMatchingRiskFactor +} + +// addQuery adds a query to the graph. It will add the query, its variants, and connect the query to the variants +func (b *resolvedPolicyBuilder) addQuery(query *explorer.Mquery) (string, bool) { + action := b.actionOverrides[query.Mrn] + impact := b.impactOverrides[query.Mrn] + queryType := b.queryTypes[query.Mrn] + + if !canRun(action) { + return "", false + } + + if len(query.Variants) != 0 { + // If we have variants, we need to find the first matching variant. + // We will also recursively find the code id of the query that will + // be run + var matchingVariant *explorer.Mquery + var selectedCodeId string + for _, v := range query.Variants { + q, ok := b.bundleMap.Queries[v.Mrn] + if !ok { + log.Warn().Str("mrn", v.Mrn).Msg("variant not found in bundle") + continue + } + if codeId, added := b.addQuery(q); added { + // The first matching variant is selected + matchingVariant = q + selectedCodeId = codeId + break + } + } + + if matchingVariant == nil { + return "", false + } + + b.propsCache.Add(query.Props...) + b.propsCache.Add(matchingVariant.Props...) + + // Add node for query + b.addNode(&rpBuilderGenericQueryNode{queryMrn: query.Mrn, selectedCodeId: selectedCodeId, queryType: queryType}) + + // Add edge from variant to query + b.addEdge(matchingVariant.Mrn, query.Mrn, impact) + + return selectedCodeId, true + } else { + if !b.anyFilterMatches(query.Filters) { + return "", false + } + + b.propsCache.Add(query.Props...) + + // Add node for execution query + b.addNode(&rpBuilderExecutionQueryNode{query: query}) + // Add node for query + b.addNode(&rpBuilderGenericQueryNode{queryMrn: query.Mrn, selectedCodeId: query.CodeId, queryType: queryType}) + + // Add edge from execution query to query + b.addEdge(query.CodeId, query.Mrn, impact) + + return query.CodeId, true + } +} + +// addRiskFactor adds a risk factor to the graph. It will add the risk factor, its checks, and connect the checks to the risk factor +func (b *resolvedPolicyBuilder) addRiskFactor(riskFactor *RiskFactor) (bool, error) { + action := b.actionOverrides[riskFactor.Mrn] + if !canRun(action) { + return false, nil + } + + if !b.anyFilterMatches(riskFactor.Filters) { + return false, nil + } + + selectedCodeIds := make([]string, 0, len(riskFactor.Checks)) + for _, c := range riskFactor.Checks { + if len(c.Variants) != 0 { + return false, fmt.Errorf("risk factor checks cannot have variants") + } + if !b.anyFilterMatches(c.Filters) { + continue + } + + b.propsCache.Add(c.Props...) + + // Add node for execution query + b.addNode(&rpBuilderExecutionQueryNode{query: c}) + // TODO: we should just score the risk factor normally, I don't know why we ignore the score + b.addEdge(c.CodeId, riskFactor.Mrn, &explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + + selectedCodeIds = append(selectedCodeIds, c.CodeId) + + // TODO: we cannot use addQuery here because of the way cnspec tries to filter out + // sending scores for queries that are risk factors. This code, which is in collector.go + // needs to be refactored in such a way that it is natively integrated into the graph + // the does the processing of the scores. The current implementation has a problem if + // we have a child job on the risk factor that is mrn of the query. + // if selectedCodeId, ok := b.addQuery(c); ok { + // selectedCodeIds = append(selectedCodeIds, selectedCodeId) + // b.addEdge(c.Mrn, riskFactor.Mrn, &explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + // } + } + + if len(selectedCodeIds) == 0 { + return false, nil + } + + b.addNode(&rpBuilderRiskFactorNode{riskFactor: riskFactor, magnitude: b.riskMagnitudes[riskFactor.Mrn], selectedCodeIds: selectedCodeIds}) + + return true, nil +} + +func (b *resolvedPolicyBuilder) anyFilterMatches(f *explorer.Filters) bool { + return f.Supports(b.assetFilters) +} + +// addFramework adds a framework to the graph. It will add the framework, its dependent frameworks, its controls, and connect +// the controls to the framework +func (b *resolvedPolicyBuilder) addFramework(framework *Framework) bool { + action := b.actionOverrides[framework.Mrn] + if !canRun(action) { + return false + } + + // Create a node for the framework, but only if its a valid framework mrn + // Otherwise, we have the asset / space policies which we will connect + // to. We need to do this because we cannot have a space framework and space + // policy reporting job because they would have the same qr id. + // If the node already exists, its represented by the asset or space policy + // and is not a valid framework mrn + var impact *explorer.Impact + if _, ok := b.nodes[framework.Mrn]; !ok { + b.addNode(&rpBuilderFrameworkNode{frameworkMrn: framework.Mrn}) + } else { + impact = &explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE} + } + + for _, fmap := range framework.FrameworkMaps { + for _, control := range fmap.Controls { + if b.addControl(control) { + b.addEdge(control.Mrn, fmap.FrameworkOwner.Mrn, b.actionToImpact(control.Mrn)) + } + } + } + + for _, fdep := range framework.Dependencies { + f, ok := b.bundleMap.Frameworks[fdep.Mrn] + if !ok { + log.Warn().Str("mrn", fdep.Mrn).Msg("framework not found in bundle") + continue + } + if b.addFramework(f) { + b.addEdge(fdep.Mrn, framework.Mrn, impact) + } + } + + return true +} + +// addControl adds a control to the graph and connect policies, controls, checks, and queries to the control +func (b *resolvedPolicyBuilder) addControl(control *ControlMap) bool { + action := b.actionOverrides[control.Mrn] + if !canRun(action) { + return false + } + + hasChild := false + + for _, q := range control.Checks { + if _, ok := b.nodes[q.Mrn]; ok { + n := b.nodes[q.Mrn] + if n == nil { + continue + } + qNode, ok := n.(*rpBuilderGenericQueryNode) + if ok { + b.addEdge(qNode.selectedCodeId, control.Mrn, b.actionToImpact(q.Mrn)) + hasChild = true + } + } + } + + for _, q := range control.Queries { + if _, ok := b.nodes[q.Mrn]; ok { + n := b.nodes[q.Mrn] + if n == nil { + continue + } + qNode, ok := n.(*rpBuilderGenericQueryNode) + if ok { + b.addEdge(qNode.selectedCodeId, control.Mrn, nil) + hasChild = true + } + } + } + + for _, p := range control.Policies { + if _, ok := b.nodes[p.Mrn]; ok { + // Add the edge from the control to the policy + b.addEdge(p.Mrn, control.Mrn, b.actionToImpact(p.Mrn)) + hasChild = true + } + } + + for _, c := range control.Controls { + // We will just assume that the control is in the graph + // If its not, it will get filtered out later when we build + // the resolved policy + // Doing this so we don't need to topologically sort the dependency + // tree for the controls + b.addEdge(c.Mrn, control.Mrn, b.actionToImpact(c.Mrn)) + hasChild = true + } + + if hasChild { + // Add node for control + b.addNode(&rpBuilderControlNode{controlMrn: control.Mrn}) + } + + return true +} + +func (b *resolvedPolicyBuilder) actionToImpact(mrn string) *explorer.Impact { + action := b.actionOverrides[mrn] + if action == explorer.Action_IGNORE { + return &explorer.Impact{ + Scoring: explorer.ScoringSystem_IGNORE_SCORE, + } + } + return nil +} + +func addReportingJob(qrId string, qrIdIsMrn bool, uuid string, typ ReportingJob_Type, rp *ResolvedPolicy) *ReportingJob { + if _, ok := rp.CollectorJob.ReportingJobs[uuid]; !ok { + rp.CollectorJob.ReportingJobs[uuid] = &ReportingJob{ + QrId: qrId, + Uuid: uuid, + ChildJobs: map[string]*explorer.Impact{}, + Datapoints: map[string]bool{}, + Type: typ, + } + if qrIdIsMrn { + rp.CollectorJob.ReportingJobs[uuid].Mrns = []string{qrId} + } + } + return rp.CollectorJob.ReportingJobs[uuid] +} + +func compileProps(query *explorer.Mquery, rp *ResolvedPolicy, data *rpBuilderData) (map[string]*llx.Primitive, map[string]string, error) { + var propTypes map[string]*llx.Primitive + var propToChecksums map[string]string + if len(query.Props) != 0 { + propTypes = make(map[string]*llx.Primitive, len(query.Props)) + propToChecksums = make(map[string]string, len(query.Props)) + for j := range query.Props { + prop := query.Props[j] + + // we only get this if there is an override higher up in the policy + override, name, _ := data.propsCache.Get(prop.Mrn) + if override != nil { + prop = override + } + if name == "" { + var err error + name, err = mrn.GetResource(prop.Mrn, MRN_RESOURCE_QUERY) + if err != nil { + return nil, nil, errors.New("failed to get property name") + } + } + + executionQuery, dataChecksum, err := mquery2executionQuery(prop, nil, map[string]string{}, rp.CollectorJob, false, data.compilerConf) + if err != nil { + return nil, nil, errors.New("resolver> failed to compile query for MRN " + prop.Mrn + ": " + err.Error()) + } + if dataChecksum == "" { + return nil, nil, errors.New("property returns too many value, cannot determine entrypoint checksum: '" + prop.Mql + "'") + } + rp.ExecutionJob.Queries[prop.CodeId] = executionQuery + + propTypes[name] = &llx.Primitive{Type: prop.Type} + propToChecksums[name] = dataChecksum + } + } + return propTypes, propToChecksums, nil +} + +// connectReportingJobNotifies adds the notifies and child jobs links in the reporting jobs +func connectReportingJobNotifies(child *ReportingJob, parent *ReportingJob, impact *explorer.Impact) { + for _, n := range child.Notify { + if n == parent.Uuid { + fmt.Println("already connected") + } + } + child.Notify = append(child.Notify, parent.Uuid) + parent.ChildJobs[child.Uuid] = impact +} + +// normalizeAction normalizes the action based on the group type and impact. We need to do this because +// we've had different ways of representing actions in the past and we need to normalize them to the current +func normalizeAction(groupType GroupType, action explorer.Action, impact *explorer.Impact) explorer.Action { + switch groupType { + case GroupType_DISABLE: + return explorer.Action_DEACTIVATE + case GroupType_OUT_OF_SCOPE: + return explorer.Action_OUT_OF_SCOPE + case GroupType_IGNORED: + return explorer.Action_IGNORE + default: + if impact != nil && impact.Scoring == explorer.ScoringSystem_IGNORE_SCORE { + return explorer.Action_IGNORE + } + return action + } +} + +func isOverride(action explorer.Action, groupType GroupType) bool { + return action != explorer.Action_UNSPECIFIED || + groupType == GroupType_DISABLE || + groupType == GroupType_OUT_OF_SCOPE || + groupType == GroupType_IGNORED +} diff --git a/policy/resolver.go b/policy/resolver.go index bc12c8a3..2d4ebb66 100644 --- a/policy/resolver.go +++ b/policy/resolver.go @@ -32,7 +32,8 @@ const ( // This is used to change the checksum of the resolved policy when we want it to be recalculated // This can be updated, e.g., when we change how the report jobs are generated // A change of this string will force an update of all the stored resolved policies - RESOLVER_VERSION = "v2024-08-29" + RESOLVER_VERSION = "v2024-08-29" + RESOLVER_VERSION_NG = "v2024-11-11" ) type AssetMutation struct { @@ -451,6 +452,16 @@ func (s *LocalServices) resolve(ctx context.Context, policyMrn string, assetFilt return nil, errors.New("concurrent policy resolve") } +type nextGenResolverFeature struct{} + +func WithNextGenResolver(context.Context) context.Context { + return context.WithValue(context.Background(), nextGenResolverFeature{}, true) +} + +func IsNextGenResolver(ctx context.Context) bool { + return ctx.Value(nextGenResolverFeature{}) != nil +} + func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetFilters []*explorer.Mquery) (*ResolvedPolicy, error) { logCtx := logger.FromContext(ctx) now := time.Now() @@ -477,11 +488,12 @@ func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetF if err != nil { return nil, err } + bundleMap := bundle.ToMap() frameworkObj := bundleMap.Frameworks[bundleMrn] policyObj := bundleMap.Policies[bundleMrn] - resolvedPolicyExecutionChecksum := BundleExecutionChecksum(policyObj, frameworkObj) + resolvedPolicyExecutionChecksum := BundleExecutionChecksum(ctx, policyObj, frameworkObj) matchingFilters, err := MatchingAssetFilters(bundleMrn, assetFilters, policyObj) if err != nil { @@ -491,6 +503,20 @@ func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetF return nil, explorer.NewAssetMatchError(bundleMrn, "policies", "no-matching-policy", assetFilters, policyObj.ComputedFilters) } + if IsNextGenResolver(ctx) { + resolvedPolicy, err := buildResolvedPolicy(ctx, bundleMrn, bundle, matchingFilters, time.Now(), conf) + if err != nil { + return nil, err + } + + err = s.DataLake.SetResolvedPolicy(ctx, bundleMrn, resolvedPolicy, V2Code, false) + if err != nil { + return nil, err + } + + return resolvedPolicy, nil + } + assetFiltersMap := make(map[string]struct{}, len(matchingFilters)) for i := range matchingFilters { assetFiltersMap[matchingFilters[i].CodeId] = struct{}{} @@ -519,7 +545,7 @@ func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetF Msg("resolver> phase 1: no cached result, resolve the bundle now") cache := &resolverCache{ - baseChecksum: BundleExecutionChecksum(policyObj, frameworkObj), + baseChecksum: BundleExecutionChecksum(ctx, policyObj, frameworkObj), assetFiltersChecksum: assetFiltersChecksum, assetFilters: assetFiltersMap, executionQueries: map[string]*ExecutionQuery{}, @@ -655,7 +681,7 @@ func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetF Msg("resolver> phase 5: resolve controls [ok]") // phase 6: refresh all checksums - s.refreshChecksums(executionJob, collectorJob) + refreshChecksums(executionJob, collectorJob) // the final phases are done in the DataLake for _, rj := range collectorJob.ReportingJobs { @@ -679,7 +705,7 @@ func (s *LocalServices) tryResolve(ctx context.Context, bundleMrn string, assetF return &resolvedPolicy, nil } -func (s *LocalServices) refreshChecksums(executionJob *ExecutionJob, collectorJob *CollectorJob) { +func refreshChecksums(executionJob *ExecutionJob, collectorJob *CollectorJob) { // execution job { queryKeys := sortx.Keys(executionJob.Queries) diff --git a/policy/resolver_test.go b/policy/resolver_test.go index 2b45811c..e441db81 100644 --- a/policy/resolver_test.go +++ b/policy/resolver_test.go @@ -71,6 +71,10 @@ func queryMrn(uid string) string { return "//test.sth/queries/" + uid } +func riskFactorMrn(uid string) string { + return "//test.sth/risks/" + uid +} + func TestResolve_EmptyPolicy(t *testing.T) { b := parseBundle(t, ` owner_mrn: //test.sth diff --git a/policy/resolver_v2_test.go b/policy/resolver_v2_test.go new file mode 100644 index 00000000..fb7b59d2 --- /dev/null +++ b/policy/resolver_v2_test.go @@ -0,0 +1,2010 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package policy_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mondoo.com/cnquery/v11/explorer" + "go.mondoo.com/cnquery/v11/mqlc" + "go.mondoo.com/cnquery/v11/providers" + "go.mondoo.com/cnspec/v11/internal/datalakes/inmemory" + "go.mondoo.com/cnspec/v11/policy" +) + +func collectQueriesFromRiskFactors(p *policy.Policy, query map[string]*explorer.Mquery) { + for _, rf := range p.RiskFactors { + for _, c := range rf.Checks { + query[c.Mrn] = c + } + } +} + +func newResolvedPolicyTester(bundle *policy.Bundle, conf mqlc.CompilerConfig) *resolvedPolicyTester { + m := bundle.ToMap() + for _, p := range m.Policies { + collectQueriesFromRiskFactors(p, m.Queries) + } + + return &resolvedPolicyTester{ + bundleMap: m, + items: []resolvedPolicyTesterItem{}, + conf: conf, + } +} + +type resolvedPolicyTesterItem interface { + testIt(t *testing.T, resolvedPolicy *policy.ResolvedPolicy) +} + +type resolvedPolicyTester struct { + bundleMap *policy.PolicyBundleMap + items []resolvedPolicyTesterItem + conf mqlc.CompilerConfig +} + +func (r *resolvedPolicyTester) doTest(t *testing.T, rp *policy.ResolvedPolicy) { + for _, item := range r.items { + item.testIt(t, rp) + } +} + +func (r *resolvedPolicyTester) ExecutesQuery(mrn string) *resolvedPolicyTesterExecutesQueryBuilder { + item := &resolvedPolicyTesterExecutesQueryBuilder{tester: r, mrn: mrn} + r.items = append(r.items, item) + return item +} + +func (r *resolvedPolicyTester) DoesNotExecutesQuery(mrn string) { + item := &resolvedPolicyTesterExecutesQueryBuilder{tester: r, mrn: mrn, doesNotExecute: true} + r.items = append(r.items, item) +} + +type resolvedPolicyTesterExecutesQueryBuilder struct { + tester *resolvedPolicyTester + mrn string + datapoints *[]string + props *map[string]string + doesNotExecute bool +} + +func (r *resolvedPolicyTesterExecutesQueryBuilder) WithProps(props map[string]string) *resolvedPolicyTesterExecutesQueryBuilder { + r.props = &props + return r +} + +func (r *resolvedPolicyTesterExecutesQueryBuilder) testIt(t *testing.T, rp *policy.ResolvedPolicy) { + q := r.tester.bundleMap.Queries[r.mrn] + require.NotNilf(t, q, "query not found in bundle: %s", r.mrn) + codeId := q.CodeId + require.NotEmptyf(t, codeId, "query %s doesn't have code id", r.mrn) + + eq := rp.ExecutionJob.Queries[codeId] + if r.doesNotExecute { + require.Nil(t, eq, "query %s should not be executed", r.mrn) + return + } + require.NotNilf(t, eq, "query %s not found in ExecutionJob", r.mrn) + + if r.datapoints != nil { + require.ElementsMatchf(t, *r.datapoints, eq.Datapoints, "datapoints mismatch for query %q", r.mrn) + } + + if r.props != nil { + require.Lenf(t, eq.Properties, len(*r.props), "properties mismatch for query %q", r.mrn) + for propName, mql := range *r.props { + // Compile the property + codeBundle, err := mqlc.Compile(mql, nil, r.tester.conf) + require.NoErrorf(t, err, "failed to compile property %q for query %q", propName, r.mrn) + propCodeId := codeBundle.CodeV2.Id + require.NotEmptyf(t, propCodeId, "property %s doesn't have code id", propName) + propEq := rp.ExecutionJob.Queries[propCodeId] + require.NotNilf(t, propEq, "property %q not found in ExecutionJob with code id %q", propName, propCodeId) + require.Lenf(t, propEq.Datapoints, 1, "property %q should have exactly one datapoint", propName) + propDatapoint := propEq.Datapoints[0] + require.Equalf(t, eq.Properties[propName], propDatapoint, "property %q value mismatch", propName) + } + } +} + +type resolvedPolicyTesterReportingJobNotifiesBuilder struct { + rjTester *resolvedPolicyTesterReportingJobBuilder + childMrn string + childMrnForCodeId string + parent string + impact *explorer.Impact + impactSet bool +} + +func (r *resolvedPolicyTesterReportingJobNotifiesBuilder) WithImpact(impact *explorer.Impact) *resolvedPolicyTesterReportingJobNotifiesBuilder { + r.impact = impact + r.impactSet = true + return r +} + +func findReportingJobByQrId(rp *policy.ResolvedPolicy, qrId string) *policy.ReportingJob { + for _, rj := range rp.CollectorJob.ReportingJobs { + if rj.QrId == qrId { + return rj + } + } + return nil +} + +func (r *resolvedPolicyTesterReportingJobNotifiesBuilder) testIt(t *testing.T, rp *policy.ResolvedPolicy) { + var qrId string + var extraInfo string + mrnsMatchesQrId := false + if r.childMrn != "" { + qrId = r.childMrn + mrnsMatchesQrId = true + } else { + q := r.rjTester.tester.bundleMap.Queries[r.childMrnForCodeId] + require.NotNilf(t, q, "query not found in bundle: %s", r.childMrnForCodeId) + require.NotEmptyf(t, q.CodeId, "query %s doesn't have code id", r.childMrnForCodeId) + qrId = q.CodeId + extraInfo = " (" + r.childMrnForCodeId + ")" + } + childRj := findReportingJobByQrId(rp, qrId) + require.NotNilf(t, childRj, "child reporting job %s%s not found", qrId, extraInfo) + + if mrnsMatchesQrId { + require.Equalf(t, []string{qrId}, childRj.Mrns, "child reporting job %s%s mrns mismatch", qrId, extraInfo) + } + + parentRj := findReportingJobByQrId(rp, r.parent) + require.NotNilf(t, parentRj, "parent reporting job %s not found", r.parent) + require.Containsf(t, childRj.Notify, parentRj.Uuid, "child reporting job %s%s doesn't notify parent reporting job %s", qrId, extraInfo, r.parent) + + require.Containsf(t, parentRj.ChildJobs, childRj.Uuid, "parent reporting job %s doesn't have child reporting job %s%s", r.parent, qrId, extraInfo) + if r.impactSet { + require.EqualExportedValuesf(t, r.impact, parentRj.ChildJobs[childRj.Uuid], "impact mismatch for child reporting job %s%s", qrId, extraInfo) + } + +} + +type resolvedPolicyTesterReportingJobBuilder struct { + tester *resolvedPolicyTester + mrn string + mrnForCodeId string + typ *policy.ReportingJob_Type + scoringSystem *explorer.ScoringSystem + notifies []*resolvedPolicyTesterReportingJobNotifiesBuilder + notifiesSet bool + doesNotExist bool +} + +func (r *resolvedPolicyTester) CodeIdReportingJobForMrn(mrn string) *resolvedPolicyTesterReportingJobBuilder { + var item *resolvedPolicyTesterReportingJobBuilder + for _, existing := range r.items { + if existingItem, ok := existing.(*resolvedPolicyTesterReportingJobBuilder); ok && existingItem.mrnForCodeId == mrn { + item = existingItem + break + } + } + if item == nil { + item = &resolvedPolicyTesterReportingJobBuilder{tester: r, mrnForCodeId: mrn} + r.items = append(r.items, item) + } + + return item +} + +func (r *resolvedPolicyTester) ReportingJobByMrn(mrn string) *resolvedPolicyTesterReportingJobBuilder { + var item *resolvedPolicyTesterReportingJobBuilder + for _, existing := range r.items { + if existingItem, ok := existing.(*resolvedPolicyTesterReportingJobBuilder); ok && existingItem.mrn == mrn { + item = existingItem + break + } + } + if item == nil { + item = &resolvedPolicyTesterReportingJobBuilder{tester: r, mrn: mrn} + r.items = append(r.items, item) + } + return item +} + +func (r *resolvedPolicyTesterReportingJobBuilder) DoesNotExist() { + r.doesNotExist = true +} + +func (r *resolvedPolicyTesterReportingJobBuilder) WithType(typ policy.ReportingJob_Type) *resolvedPolicyTesterReportingJobBuilder { + r.typ = &typ + return r +} + +func (r *resolvedPolicyTesterReportingJobBuilder) Notifies(qrId string) *resolvedPolicyTesterReportingJobNotifiesBuilder { + n := &resolvedPolicyTesterReportingJobNotifiesBuilder{rjTester: r, childMrnForCodeId: r.mrnForCodeId, childMrn: r.mrn, parent: qrId} + r.notifies = append(r.notifies, n) + r.notifiesSet = true + return n +} + +func (r *resolvedPolicyTesterReportingJobBuilder) WithScoringSystem(scoringSystem explorer.ScoringSystem) *resolvedPolicyTesterReportingJobBuilder { + r.scoringSystem = &scoringSystem + return r +} + +func (r *resolvedPolicyTesterReportingJobBuilder) testIt(t *testing.T, rp *policy.ResolvedPolicy) { + var qrId string + var extraInfo string + if r.mrn != "" { + qrId = r.mrn + } else { + q := r.tester.bundleMap.Queries[r.mrnForCodeId] + require.NotNilf(t, q, "query not found in bundle: %s", r.mrnForCodeId) + require.NotEmptyf(t, q.CodeId, "query %s doesn't have code id", r.mrnForCodeId) + qrId = q.CodeId + extraInfo = " (" + r.mrnForCodeId + ")" + } + + rj := findReportingJobByQrId(rp, qrId) + if r.doesNotExist { + require.Nilf(t, rj, "reporting job %s%s should not exist", qrId, extraInfo) + return + } + require.NotNilf(t, rj, "reporting job %s%s not found", qrId, extraInfo) + + if r.typ != nil { + require.Equalf(t, *r.typ, rj.Type, "reporting job %s%s type mismatch", qrId, extraInfo) + } + + if r.scoringSystem != nil { + require.Equalf(t, *r.scoringSystem, rj.ScoringSystem, "reporting job %s%s scoring system mismatch", qrId, extraInfo) + } + + if r.notifiesSet { + for _, n := range r.notifies { + n.testIt(t, rp) + } + require.Len(t, rj.Notify, len(r.notifies), "reporting job uuid=%s qrId=%s%s notify mismatch", rj.Uuid, qrId, extraInfo) + } +} + +func contextResolverV2() context.Context { + return policy.WithNextGenResolver(context.Background()) +} + +func TestResolveV2_EmptyPolicy(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve w/o filters", func(t *testing.T) { + _, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + }) + assert.EqualError(t, err, "rpc error: code = InvalidArgument desc = asset doesn't support any policies") + }) + + t.Run("resolve with empty filters", func(t *testing.T) { + _, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{}}, + }) + assert.EqualError(t, err, "failed to compile query: failed to compile query '': query is not implemented ''") + }) + + t.Run("resolve with random filters", func(t *testing.T) { + _, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + assert.EqualError(t, err, + "rpc error: code = InvalidArgument desc = asset isn't supported by any policies\n"+ + "policies didn't provide any filters\n"+ + "asset supports: true\n") + }) +} + +func TestResolveV2_SimplePolicy(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + queries: + - uid: query1 + mql: asset{*} +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + require.Len(t, rp.ExecutionJob.Queries, 3) + require.Len(t, rp.Filters, 1) + require.Len(t, rp.CollectorJob.ReportingJobs, 5) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies("root") + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies("root") + + rpTester.doTest(t, rp) + }) + + t.Run("resolve with many filters (one is correct)", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{ + {Mql: "asset.family.contains(\"linux\")"}, + {Mql: "true"}, + {Mql: "asset.family.contains(\"windows\")"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, rp) + }) + + t.Run("resolve with incorrect filters", func(t *testing.T) { + _, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{ + {Mql: "asset.family.contains(\"linux\")"}, + {Mql: "false"}, + {Mql: "asset.family.contains(\"windows\")"}, + }, + }) + assert.EqualError(t, err, + "rpc error: code = InvalidArgument desc = asset isn't supported by any policies\n"+ + "policies support: true\n"+ + "asset supports: asset.family.contains(\"linux\"), asset.family.contains(\"windows\"), false\n") + }) +} + +func TestResolveV2_PolicyWithImpacts(t *testing.T) { + // For impacts, we always find the worst impact specified for a query in a policy bundle. + // All instances of the query use that impact + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy1 + - uid: policy2 + action: 4 +- uid: policy1 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + - uid: check2 + impact: 10 + - uid: check3 + impact: 60 + queries: + - uid: query1 +- uid: policy2 + groups: + - type: chapter + filters: "true" + checks: + - uid: check2 + impact: 5 + - uid: check3 + impact: 80 +queries: +- uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" +- uid: check2 + mql: true == false + impact: 70 +- uid: check3 + mql: true == true + impact: 9 +- uid: query1 + mql: asset{*} +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1"), policyMrn("policy2")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.ExecutesQuery(queryMrn("check2")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check2")).Notifies(queryMrn("check2")).WithImpact(&explorer.Impact{Value: &explorer.ImpactValue{Value: 70}}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check3")).Notifies(queryMrn("check3")).WithImpact(&explorer.Impact{Value: &explorer.ImpactValue{Value: 80}}) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("check2")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("check3")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("check2")).Notifies(policyMrn("policy2")) + rpTester.ReportingJobByMrn(queryMrn("check3")).Notifies(policyMrn("policy2")) + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyWithScoringSystem(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy1 +- uid: policy1 + scoring_system: highest impact + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + queries: + - uid: query1 + mql: asset{*} +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(policyMrn("policy1")).WithScoringSystem(explorer.ScoringSystem_WORST).Notifies("root") + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyWithScoringSystemOverride(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy1 + scoring_system: banded +- uid: policy1 + scoring_system: highest impact + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + queries: + - uid: query1 + mql: asset{*} +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(policyMrn("policy1")).WithScoringSystem(explorer.ScoringSystem_BANDED).Notifies("root") + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyActionIgnore(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy-active + - uid: policy-ignored + action: 4 +- uid: policy-active + owner_mrn: //test.sth + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == "definitely not the asset name" + queries: + - uid: query1 + mql: asset.arch +- uid: policy-ignored + owner_mrn: //test.sth + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == "definitely not the asset name" + queries: + - uid: query1 + mql: asset.arch +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy-active"), policyMrn("policy-ignored")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with ignored policy", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy-active")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy-active")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy-ignored")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy-ignored")) + rpTester.ReportingJobByMrn(policyMrn("policy-active")).Notifies("root") + rpTester.ReportingJobByMrn(policyMrn("policy-ignored")).Notifies("root").WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyActionScoringSystem(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy-active + scoring_system: 6 + - uid: policy-ignored + action: 4 +- uid: policy-active + owner_mrn: //test.sth + scoring_system: 2 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == "definitely not the asset name" + queries: + - uid: query1 + mql: asset.arch +- uid: policy-ignored + owner_mrn: //test.sth + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == "definitely not the asset name" + queries: + - uid: query1 + mql: asset.arch +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy-active"), policyMrn("policy-ignored")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with scoring system", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy-active")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy-active")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy-ignored")) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy-ignored")) + rpTester.ReportingJobByMrn(policyMrn("policy-active")).WithScoringSystem(explorer.ScoringSystem_BANDED).Notifies("root") + rpTester.ReportingJobByMrn(policyMrn("policy-ignored")).Notifies("root").WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_IgnoredQuery(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy-1 + owner_mrn: //test.sth + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: 1 == 1 +- mrn: asset1 + owner_mrn: //test.sth + groups: + - policies: + - uid: policy-1 + - checks: + - uid: check1 + action: 4 +`) + + _, srv, err := inmemory.NewServices(providers.DefaultRuntime(), nil) + require.NoError(t, err) + + _, err = srv.SetBundle(ctx, b) + require.NoError(t, err) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies("policy-1").WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + rpTester.ReportingJobByMrn(policyMrn("policy-1")).Notifies("root") +} + +func TestResolveV2_Frameworks(t *testing.T) { + ctx := contextResolverV2() + bundleStr := ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - filters: "true" + checks: + - uid: check-fail + mql: 1 == 2 + - uid: check-pass-1 + mql: 1 == 1 + - uid: check-pass-2 + mql: 2 == 2 + queries: + - uid: active-query + title: users + mql: users + - uid: active-query-2 + title: users length + mql: users.length + - uid: check-overlap + title: overlaps with check + mql: 1 == 1 +- uid: policy-inactive + groups: + - filters: "false" + checks: + - uid: inactive-fail + mql: 1 == 2 + - uid: inactive-pass + mql: 1 == 1 + - uid: inactive-pass-2 + mql: 2 == 2 + queries: + - uid: inactive-query + title: users group + mql: users { group} +frameworks: +- uid: framework1 + name: framework1 + groups: + - title: group1 + controls: + - uid: control1 + title: control1 + - uid: control2 + title: control2 + - uid: control3 + title: control3 + - uid: control4 + title: control4 + - uid: control5 + title: control5 +- uid: framework2 + name: framework2 + groups: + - title: group1 + controls: + - uid: control1 + title: control1 + - uid: control2 + title: control2 +- uid: parent-framework + dependencies: + - mrn: ` + frameworkMrn("framework1") + ` + +framework_maps: +- uid: framework-map1 + framework_owner: + uid: framework1 + policy_dependencies: + - uid: policy1 + controls: + - uid: control1 + checks: + - uid: check-pass-1 + queries: + - uid: active-query + - uid: active-query-2 + - uid: control2 + checks: + - uid: check-pass-2 + - uid: check-fail + - uid: control4 + controls: + - uid: control1 +- uid: framework-map2 + framework_owner: + uid: framework1 + policy_dependencies: + - uid: policy1 + controls: + - uid: control4 + controls: + - uid: control1 + - uid: control5 + controls: + - uid: control1 +` + + t.Run("resolve with correct filters", func(t *testing.T) { + b := parseBundle(t, bundleStr) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1"), policyMrn("policy-inactive")}, frameworks: []string{frameworkMrn("parent-framework")}}, + }, []*policy.Bundle{b}) + + bundle, err := srv.GetBundle(ctx, &policy.Mrn{Mrn: "asset1"}) + require.NoError(t, err) + + bundleMap, err := bundle.Compile(ctx, conf.Schema, nil) + require.NoError(t, err) + + mrnToQueryId := map[string]string{} + for _, q := range bundleMap.Queries { + mrnToQueryId[q.Mrn] = q.CodeId + } + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("check-fail")) + rpTester.ExecutesQuery(queryMrn("check-pass-1")) + rpTester.ExecutesQuery(queryMrn("check-pass-2")) + rpTester.ExecutesQuery(queryMrn("active-query")) + rpTester.ExecutesQuery(queryMrn("active-query-2")) + rpTester.ExecutesQuery(queryMrn("check-overlap")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check-fail")).Notifies(queryMrn("check-fail")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check-fail")).Notifies(controlMrn("control2")) + rpTester.ReportingJobByMrn(queryMrn("check-fail")).Notifies(policyMrn("policy1")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check-pass-1")).Notifies(queryMrn("check-pass-1")) + // This is a limitaion of the test framework. We lookup the code id from check-pass-1 because + // we need 1 tester that has all the notifies + rpTester.CodeIdReportingJobForMrn(queryMrn("check-pass-1")).Notifies(queryMrn("check-overlap")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check-pass-1")).Notifies(controlMrn("control1")) + rpTester.ReportingJobByMrn(queryMrn("check-pass-1")).Notifies(policyMrn("policy1")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check-pass-2")).Notifies(queryMrn("check-pass-2")) + rpTester.CodeIdReportingJobForMrn(queryMrn("check-pass-2")).Notifies(controlMrn("control2")) + rpTester.ReportingJobByMrn(queryMrn("check-pass-2")).Notifies(policyMrn("policy1")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(queryMrn("active-query")) + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(controlMrn("control1")) + rpTester.ReportingJobByMrn(queryMrn("active-query")).Notifies(policyMrn("policy1")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query-2")).Notifies(queryMrn("active-query-2")) + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query-2")).Notifies(controlMrn("control1")) + rpTester.ReportingJobByMrn(queryMrn("active-query-2")).Notifies(policyMrn("policy1")) + + rpTester.ReportingJobByMrn(queryMrn("check-overlap")).Notifies(policyMrn("policy1")) + + rpTester.ReportingJobByMrn(controlMrn("control1")).Notifies(controlMrn("control4")) + rpTester.ReportingJobByMrn(controlMrn("control1")).Notifies(controlMrn("control5")) + rpTester.ReportingJobByMrn(controlMrn("control1")).Notifies(frameworkMrn("framework1")) + rpTester.ReportingJobByMrn(controlMrn("control2")).Notifies(frameworkMrn("framework1")) + rpTester.ReportingJobByMrn(controlMrn("control4")).Notifies(frameworkMrn("framework1")) + + rpTester.ReportingJobByMrn(policyMrn("policy1")).Notifies("root") + rpTester.ReportingJobByMrn(frameworkMrn("framework1")).Notifies(frameworkMrn("parent-framework")) + rpTester.ReportingJobByMrn(frameworkMrn("parent-framework")).Notifies("root") + + rpTester.doTest(t, rp) + }) + + t.Run("test resolving with inactive data queries", func(t *testing.T) { + // test that creating a bundle with inactive data queries (where the packs/policies are inactive) + // will still end up in a successfully resolved policy for the asset + bundleStr := ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - filters: "true" + queries: + - uid: active-query + title: users + mql: users +- uid: policy-inactive + groups: + - filters: "false" + queries: + - uid: inactive-query + title: users group + mql: users { group} +frameworks: +- uid: framework1 + name: framework1 + groups: + - title: group1 + controls: + - uid: control1 + title: control1 + - uid: control2 + title: control2 +- uid: parent-framework + dependencies: + - mrn: ` + frameworkMrn("framework1") + ` + +framework_maps: +- uid: framework-map1 + framework_owner: + uid: framework1 + policy_dependencies: + - uid: policy1 + - uid: policy-inactive + controls: + - uid: control1 + queries: + - uid: active-query + - uid: control2 + queries: + - uid: inactive-query +` + b := parseBundle(t, bundleStr) + + // we do not activate policy-inactive, which means that its query should not get executed + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}, frameworks: []string{frameworkMrn("parent-framework")}}, + }, []*policy.Bundle{b}) + + bundle, err := srv.GetBundle(ctx, &policy.Mrn{Mrn: "asset1"}) + require.NoError(t, err) + + bundleMap, err := bundle.Compile(ctx, conf.Schema, nil) + require.NoError(t, err) + + mrnToQueryId := map[string]string{} + for _, q := range bundleMap.Queries { + mrnToQueryId[q.Mrn] = q.CodeId + } + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("active-query")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(queryMrn("active-query")) + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(controlMrn("control1")) + rpTester.ReportingJobByMrn(queryMrn("active-query")).Notifies(policyMrn("policy1")) + + rpTester.ReportingJobByMrn(controlMrn("control1")).Notifies(frameworkMrn("framework1")) + + rpTester.ReportingJobByMrn(frameworkMrn("framework1")).Notifies(frameworkMrn("parent-framework")) + rpTester.ReportingJobByMrn(frameworkMrn("parent-framework")).Notifies("root") + rpTester.ReportingJobByMrn(policyMrn("policy1")).Notifies("root") + + rpTester.doTest(t, rp) + }) + + t.Run("test resolving with non-matching data queries", func(t *testing.T) { + // test that creating a bundle with active data queries that do not match the asset, based on the + // policy asset filters, will still create a resolved policy for the asset + bundleStr := ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - filters: "false" + queries: + - uid: query-1 + title: users + mql: users +- uid: policy2 + groups: + - filters: "true" + queries: + - uid: query-2 + title: users length + mql: users.length + +frameworks: +- uid: framework1 + name: framework1 + groups: + - title: group1 + controls: + - uid: control1 + title: control1 +- uid: parent-framework + dependencies: + - mrn: ` + frameworkMrn("framework1") + ` + +framework_maps: +- uid: framework-map1 + framework_owner: + uid: framework1 + policy_dependencies: + - uid: policy1 + - uid: policy2 + controls: + - uid: control1 + queries: + - uid: query-1 + - uid: query-2 +` + b := parseBundle(t, bundleStr) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1"), policyMrn("policy2")}, frameworks: []string{frameworkMrn("parent-framework")}}, + }, []*policy.Bundle{b}) + + bundle, err := srv.GetBundle(ctx, &policy.Mrn{Mrn: "asset1"}) + require.NoError(t, err) + + bundleMap, err := bundle.Compile(ctx, conf.Schema, nil) + require.NoError(t, err) + + mrnToQueryId := map[string]string{} + for _, q := range bundleMap.Queries { + mrnToQueryId[q.Mrn] = q.CodeId + } + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query-2")) + + rpTester.ReportingJobByMrn(queryMrn("query-1")).DoesNotExist() + + rpTester.CodeIdReportingJobForMrn(queryMrn("query-2")).Notifies(queryMrn("query-2")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query-2")).Notifies(controlMrn("control1")) + rpTester.ReportingJobByMrn(queryMrn("query-2")).Notifies(policyMrn("policy2")) + + rpTester.ReportingJobByMrn(controlMrn("control1")).Notifies(frameworkMrn("framework1")) + + rpTester.ReportingJobByMrn(frameworkMrn("framework1")).Notifies(frameworkMrn("parent-framework")) + rpTester.ReportingJobByMrn(frameworkMrn("parent-framework")).Notifies("root") + rpTester.ReportingJobByMrn(policyMrn("policy2")).Notifies("root") + + rpTester.doTest(t, rp) + }) + + t.Run("test checksumming", func(t *testing.T) { + bInitial := parseBundle(t, bundleStr) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}, frameworks: []string{frameworkMrn("parent-framework")}}, + }, []*policy.Bundle{bInitial}) + + rpInitial, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rpInitial) + + bFrameworkUpdate := parseBundle(t, bundleStr) + bFrameworkUpdate.Frameworks[0].Groups[0].Controls = bFrameworkUpdate.Frameworks[0].Groups[0].Controls[:2] + + srv = initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}, frameworks: []string{frameworkMrn("parent-framework")}}, + }, []*policy.Bundle{bFrameworkUpdate}) + + rpFrameworkUpdate, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rpFrameworkUpdate) + + require.NotEqual(t, rpInitial.GraphExecutionChecksum, rpFrameworkUpdate.GraphExecutionChecksum) + }) +} + +// TestResolve_PoliciesMatchingAgainstIncorrectPlatform tests that policies are not matched against +// assets that do not match the asset filter. It was possible that the reporting structure had +// a node for the policy, but no actual reporting job for it. To the user, this could look +// like the policy was executed. The issue was that a policy was considered matching if either +// the groups or any of its queries filters matched. This tests to ensure that if the policies +// group filtered it out, it doesn't show up in the reporting structure +func TestResolveV2_PoliciesMatchingAgainstIncorrectPlatform(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 +- uid: policy2 + groups: + - type: chapter + filters: "false" + checks: + - uid: check2 +- uid: pack1 + groups: + - type: chapter + filters: "true" + queries: + - uid: dataquery1 +- uid: pack2 + groups: + - type: chapter + filters: "false" + queries: + - uid: dataquery2 + +queries: +- uid: check1 + title: check1 + mql: true +- uid: check2 + title: check2 + filters: | + true + mql: | + 1 == 1 +- uid: dataquery1 + title: dataquery1 + mql: | + asset.name +- uid: dataquery2 + title: dataquery2 + filters: | + true + mql: | + asset.version +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1"), policyMrn("policy2"), policyMrn("pack1"), policyMrn("pack2")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(policyMrn("policy2")).DoesNotExist() + rpTester.ReportingJobByMrn(policyMrn("pack2")).DoesNotExist() + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_NeverPruneRoot(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + filters: "false" + checks: + - uid: check1 + +queries: +- uid: check1 + title: check1 + filters: | + true + mql: | + 1 == 1 +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + +} + +func TestResolveV2_PoliciesMatchingFilters(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + checks: + - uid: check1 + - uid: check2 +queries: +- uid: check1 + title: check1 + filters: + - mql: asset.name == "asset1" + - mql: asset.name == "asset2" + mql: | + asset.version +- uid: check2 + title: check2 + filters: + - mql: | + asset.name == "asset1" + asset.name == "asset2" + mql: | + asset.platform +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "asset.name == \"asset1\""}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.DoesNotExecutesQuery(queryMrn("check2")) + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_TwoMrns(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - filters: + - mql: asset.name == "asset1" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + - uid: check2 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve two MRNs to one codeID matching filter", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{Mql: "asset.name == \"asset1\""}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.ExecutesQuery(queryMrn("check2")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + // This is a limitaion of the test framework. We lookup the code id from check-pass-1 because + // we need 1 tester that has all the notifies + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check2")) + + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies("root") + rpTester.ReportingJobByMrn(queryMrn("check2")).Notifies("root") + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_TwoMrns_FilterMismatch(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + filters: + - mql: asset.name == "asset1" + - uid: check2 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + filters: + - mql: asset.name == "asset2" +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve two MRNs to one codeID matching filter", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{Mql: "asset.name == \"asset1\""}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("check1")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies("root") + rpTester.ReportingJobByMrn(queryMrn("check2")).DoesNotExist() + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_TwoMrns_DataQueries(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - filters: + - mql: asset.name == "asset1" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + - queries: + - uid: active-query + title: users + mql: users + - uid: active-query-2 + title: users length + mql: users +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve two MRNs to one codeID matching filter", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{{Mql: "asset.name == \"asset1\""}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("check1")) + rpTester.ExecutesQuery(queryMrn("active-query")) + rpTester.ExecutesQuery(queryMrn("active-query-2")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(queryMrn("active-query")) + // This is a limitaion of the test framework. We lookup the code id from active-query because + // we need 1 tester that has all the notifies + rpTester.CodeIdReportingJobForMrn(queryMrn("active-query")).Notifies(queryMrn("active-query-2")) + + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies("root") + rpTester.ReportingJobByMrn(queryMrn("active-query")).Notifies("root") + rpTester.ReportingJobByMrn(queryMrn("active-query-2")).Notifies("root") + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_TwoMrns_Variants(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - checks: + - uid: check-variants +queries: + - uid: check-variants + variants: + - uid: variant1 + - uid: variant2 + - uid: variant1 + mql: asset.name == "test1" + filters: asset.family.contains("unix") + - uid: variant2 + mql: asset.name == "test1" + filters: asset.name == "asset1" +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve two variants to different codeIDs matching filter", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("policy1"), + AssetFilters: []*explorer.Mquery{ + {Mql: "asset.name == \"asset1\""}, + {Mql: "asset.family.contains(\"unix\")"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("variant1")) + rpTester.DoesNotExecutesQuery(queryMrn("variant2")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("variant1")).Notifies(queryMrn("variant1")) + rpTester.ReportingJobByMrn(queryMrn("variant1")).Notifies("check-variants") + rpTester.ReportingJobByMrn(queryMrn("variant2")).DoesNotExist() + rpTester.ReportingJobByMrn("check-variants").Notifies("root") + }) +} + +func TestResolveV2_Variants(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: + - uid: example2 + name: Another policy + version: "1.0.0" + groups: + # Additionally it defines some queries of its own + - type: chapter + title: Some uname infos + queries: + # In this case, we are using a shared query that is defined below + - uid: uname + checks: + - uid: check-os + variants: + - uid: check-os-unix + - uid: check-os-windows + +queries: + # This is a composed query which has two variants: one for unix type systems + # and one for windows, where we don't run the additional argument. + # If you run the "uname" query, it will pick matching sub-queries for you. + - uid: uname + title: Collect uname info + variants: + - uid: unix-uname + - uid: windows-uname + - uid: unix-uname + mql: command("uname -a").stdout + filters: asset.family.contains("unix") + - uid: windows-uname + mql: command("uname").stdout + filters: asset.family.contains("windows") + + - uid: check-os-unix + filters: asset.family.contains("unix") + title: A check only run on Linux/macOS + mql: users.contains(name == "root") + - uid: check-os-windows + filters: asset.family.contains("windows") + title: A check only run on Windows + mql: users.contains(name == "Administrator")`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("example2")}}, + }, []*policy.Bundle{b}) + + _, err := srv.SetBundle(ctx, b) + require.NoError(t, err) + + _, err = b.Compile(ctx, conf.Schema, nil) + require.NoError(t, err) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: policyMrn("example2"), + AssetFilters: []*explorer.Mquery{{Mql: "asset.family.contains(\"windows\")"}}, + }) + + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ExecutesQuery(queryMrn("windows-uname")) + rpTester.ExecutesQuery(queryMrn("check-os-windows")) + rpTester.DoesNotExecutesQuery(queryMrn("unix-uname")) + rpTester.DoesNotExecutesQuery(queryMrn("check-os-unix")) + + rpTester.CodeIdReportingJobForMrn(queryMrn("windows-uname")).Notifies(queryMrn("windows-uname")) + rpTester.ReportingJobByMrn(queryMrn("windows-uname")).Notifies(queryMrn("uname")) + rpTester.ReportingJobByMrn(queryMrn("uname")).Notifies("root") + + rpTester.CodeIdReportingJobForMrn(queryMrn("check-os-windows")).Notifies(queryMrn("check-os-windows")) + rpTester.ReportingJobByMrn(queryMrn("check-os-windows")).Notifies(queryMrn("check-os")) + rpTester.ReportingJobByMrn(queryMrn("check-os")).Notifies("root") + + rpTester.doTest(t, rp) +} + +func TestResolveV2_RiskFactors(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +queries: +- uid: query-1 + title: query-1 + mql: 3 == 3 +- uid: query-2 + title: query-2 + mql: 1 == 2 +policies: + - name: testpolicy1 + uid: testpolicy1 + risk_factors: + - uid: sshd-service + magnitude: 0.9 + - uid: sshd-service-na + action: 2 + groups: + - filters: asset.name == "asset1" + checks: + - uid: query-1 + - uid: query-2 + policies: + - uid: risk-factors-security + - uid: risk-factors-security + name: Mondoo Risk Factors analysis + version: "1.0.0" + risk_factors: + - uid: sshd-service + title: SSHd Service running + indicator: asset-in-use + magnitude: 0.6 + filters: + - mql: | + asset.name == "asset1" + checks: + - uid: sshd-service-running + mql: 1 == 1 + - uid: sshd-service-na + title: SSHd Service running + indicator: asset-in-use + magnitude: 0.5 + filters: + - mql: | + asset.name == "asset1" + checks: + - uid: sshd-service-running-na + mql: 1 == 7 + - uid: not-matching + title: Not Matching + indicator: asset-in-use + magnitude: 0.5 + filters: + - mql: | + asset.name == "asset2" + checks: + - uid: not-matching + mql: true == false +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("testpolicy1")}}, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "asset.name == \"asset1\""}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ExecutesQuery(queryMrn("query-1")) + rpTester.ExecutesQuery(queryMrn("query-2")) + rpTester.ExecutesQuery(queryMrn("sshd-service-running")) + rpTester.DoesNotExecutesQuery(queryMrn("sshd-service-running-na")) + rpTester.DoesNotExecutesQuery(queryMrn("not-matching")) + rpTester.ReportingJobByMrn(queryMrn("sshd-service-running-na")).DoesNotExist() + rpTester.ReportingJobByMrn(queryMrn("not-matching")).DoesNotExist() + rpTester.ReportingJobByMrn(riskFactorMrn("not-matching")).DoesNotExist() + + rpTester.CodeIdReportingJobForMrn(queryMrn("query-1")).Notifies(queryMrn("query-1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query-2")).Notifies(queryMrn("query-2")) + + // rpTester.CodeIdReportingJobForMrn(queryMrn("sshd-service-running")).Notifies(queryMrn("sshd-service-running")) + rpTester.CodeIdReportingJobForMrn(queryMrn("sshd-service-running")).Notifies(riskFactorMrn("sshd-service")).WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + rpTester.ReportingJobByMrn(riskFactorMrn("sshd-service")).WithType(policy.ReportingJob_RISK_FACTOR).Notifies(policyMrn("risk-factors-security")) + + rpTester.ReportingJobByMrn(queryMrn("query-1")).Notifies(policyMrn("testpolicy1")) + rpTester.ReportingJobByMrn(queryMrn("query-2")).Notifies(policyMrn("testpolicy1")) + + rpTester.ReportingJobByMrn(policyMrn("testpolicy1")).Notifies("root") + + rpTester.doTest(t, rp) + + require.Equal(t, float32(0.9), rp.CollectorJob.RiskFactors[riskFactorMrn("sshd-service")].Magnitude.GetValue()) +} + +func TestResolveV2_FrameworkExceptions(t *testing.T) { + ctx := contextResolverV2() + bundleString := ` +owner_mrn: //test.sth +policies: +- uid: ssh-policy + name: SSH Policy + groups: + - filters: "true" + checks: + - uid: sshd-ciphers-01 + title: Prevent weaker CBC ciphers from being used + mql: sshd.config.ciphers.none( /cbc/ ) + impact: 60 + - uid: sshd-ciphers-02 + title: Do not allow ciphers with few bits + mql: sshd.config.ciphers.none( /128/ ) + impact: 60 + - uid: sshd-config-permissions + title: SSH config editing should be limited to admins + mql: sshd.config.file.permissions.mode == 0644 + impact: 100 + +frameworks: +- uid: mondoo-ucf + name: Unified Compliance Framework + groups: + - title: System hardening + controls: + - uid: mondoo-ucf-01 + title: Only use strong ciphers + - uid: mondoo-ucf-02 + title: Limit access to system configuration + - uid: mondoo-ucf-03 + title: Only use ciphers with sufficient bits + - title: exception-1 + type: 4 + controls: + - uid: mondoo-ucf-02 + +framework_maps: + - uid: compliance-to-ssh-policy + mrn: //test.sth/framework/compliance-to-ssh-policy + framework_owner: + uid: mondoo-ucf + policy_dependencies: + - uid: ssh-policy + controls: + - uid: mondoo-ucf-01 + checks: + - uid: sshd-ciphers-01 + - uid: sshd-ciphers-02 + - uid: mondoo-ucf-02 + checks: + - uid: sshd-config-permissions + - uid: mondoo-ucf-03 + checks: + - uid: sshd-ciphers-02 +` + + _, srv, err := inmemory.NewServices(providers.DefaultRuntime(), nil) + require.NoError(t, err) + + t.Run("resolve with ignored control", func(t *testing.T) { + b := parseBundle(t, bundleString) + + srv = initResolver(t, []*testAsset{ + { + asset: "asset1", + policies: []string{policyMrn("ssh-policy")}, + frameworks: []string{frameworkMrn("mondoo-ucf")}, + }, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(controlMrn("mondoo-ucf-02")).Notifies(frameworkMrn("mondoo-ucf")).WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + + rpTester.doTest(t, rp) + }) + + t.Run("resolve with ignored control and validUntil", func(t *testing.T) { + b := parseBundle(t, bundleString) + b.Frameworks[0].Groups[1].EndDate = time.Now().Add(time.Hour).Unix() + + srv = initResolver(t, []*testAsset{ + { + asset: "asset1", + policies: []string{policyMrn("ssh-policy")}, + frameworks: []string{frameworkMrn("mondoo-ucf")}, + }, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(controlMrn("mondoo-ucf-02")).Notifies(frameworkMrn("mondoo-ucf")).WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + + rpTester.doTest(t, rp) + }) + + t.Run("resolve with expired validUntil", func(t *testing.T) { + b := parseBundle(t, bundleString) + b.Frameworks[0].Groups[1].EndDate = time.Now().Add(-time.Hour).Unix() + + srv = initResolver(t, []*testAsset{ + { + asset: "asset1", + policies: []string{policyMrn("ssh-policy")}, + frameworks: []string{frameworkMrn("mondoo-ucf")}, + }, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(controlMrn("mondoo-ucf-02")).Notifies(frameworkMrn("mondoo-ucf")).WithImpact(nil) + + rpTester.doTest(t, rp) + }) + + t.Run("resolve with expired validUntil", func(t *testing.T) { + b := parseBundle(t, bundleString) + b.Frameworks[0].Groups[1].EndDate = time.Now().Add(time.Hour).Unix() + b.Frameworks[0].Groups[1].Type = policy.GroupType_DISABLE + b.Frameworks[0].Groups[1].ReviewStatus = policy.ReviewStatus_REJECTED + + srv = initResolver(t, []*testAsset{ + { + asset: "asset1", + policies: []string{policyMrn("ssh-policy")}, + frameworks: []string{frameworkMrn("mondoo-ucf")}, + }, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(controlMrn("mondoo-ucf-02")).Notifies(frameworkMrn("mondoo-ucf")).WithImpact(nil) + + rpTester.doTest(t, rp) + }) + + t.Run("resolve with disabled control", func(t *testing.T) { + b := parseBundle(t, bundleString) + b.Frameworks = append(b.Frameworks, &policy.Framework{ + Mrn: frameworkMrn("test"), + Dependencies: []*policy.FrameworkRef{ + { + Mrn: frameworkMrn("mondoo-ucf"), + Action: explorer.Action_ACTIVATE, + }, + }, + Groups: []*policy.FrameworkGroup{ + { + Uid: "test", + Type: policy.GroupType_DISABLE, + Controls: []*policy.Control{ + {Uid: b.Frameworks[0].Groups[0].Controls[0].Uid}, + }, + }, + }, + }) + + srv = initResolver(t, []*testAsset{ + { + asset: "asset1", + policies: []string{policyMrn("ssh-policy")}, + frameworks: []string{frameworkMrn("mondoo-ucf"), frameworkMrn("test")}, + }, + }, []*policy.Bundle{b}) + + rp, err := srv.Resolve(context.Background(), &policy.ResolveReq{ + PolicyMrn: "asset1", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + + rpTester.ReportingJobByMrn(controlMrn("mondoo-ucf-01")).DoesNotExist() + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyExceptionIgnored(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + queries: + - uid: query1 + mql: asset{*} +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy1 + - type: ignored + uid: "exceptions-1" + checks: + - uid: check1 +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.ExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.CodeIdReportingJobForMrn(queryMrn("query1")).Notifies(queryMrn("query1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy1")).WithImpact(&explorer.Impact{Scoring: explorer.ScoringSystem_IGNORE_SCORE}) + rpTester.ReportingJobByMrn(queryMrn("query1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(policyMrn("policy1")).Notifies("root") + + rpTester.doTest(t, rp) + }) +} + +func TestResolveV2_PolicyExceptionDisabled(t *testing.T) { + ctx := contextResolverV2() + b := parseBundle(t, ` +owner_mrn: //test.sth +policies: +- uid: policy1 + groups: + - type: chapter + filters: "true" + checks: + - uid: check1 + mql: asset.name == props.name + props: + - uid: name + mql: return "definitely not the asset name" + queries: + - uid: query1 + mql: asset{*} +- owner_mrn: //test.sth + mrn: //test.sth + groups: + - policies: + - uid: policy1 + - type: disable + uid: "exceptions-1" + checks: + - uid: query1 +`) + + srv := initResolver(t, []*testAsset{ + {asset: "asset1", policies: []string{policyMrn("policy1")}}, + }, []*policy.Bundle{b}) + + t.Run("resolve with correct filters", func(t *testing.T) { + rp, err := srv.Resolve(ctx, &policy.ResolveReq{ + PolicyMrn: "//test.sth", + AssetFilters: []*explorer.Mquery{{Mql: "true"}}, + }) + require.NoError(t, err) + require.NotNil(t, rp) + + rpTester := newResolvedPolicyTester(b, srv.NewCompilerConfig()) + rpTester.DoesNotExecutesQuery(queryMrn("query1")) + rpTester. + ExecutesQuery(queryMrn("check1")). + WithProps(map[string]string{"name": `return "definitely not the asset name"`}) + rpTester.CodeIdReportingJobForMrn(queryMrn("check1")).Notifies(queryMrn("check1")) + rpTester.ReportingJobByMrn(queryMrn("check1")).Notifies(policyMrn("policy1")) + rpTester.ReportingJobByMrn(policyMrn("policy1")).Notifies("root") + + rpTester.doTest(t, rp) + }) +}