Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .task/checksum/generate-ent-smart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c89c7e7c9a937583a194a2c8d524ccfb
6d449fe0a221fc086960d7c64d3c5254
2 changes: 1 addition & 1 deletion common/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/inflect v1.0.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions common/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMD
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
Expand Down
14 changes: 9 additions & 5 deletions internal/controls/trustcenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,30 @@ import (
"context"

"entgo.io/ent/dialect/sql"
"github.com/theopenlane/iam/auth"
"github.com/theopenlane/utils/rout"

"github.com/theopenlane/core/v2/internal/ent/generated"
"github.com/theopenlane/core/v2/internal/ent/generated/privacy"
"github.com/theopenlane/core/v2/internal/ent/generated/standard"
"github.com/theopenlane/core/v2/pkg/logx"
"github.com/theopenlane/iam/auth"
"github.com/theopenlane/utils/rout"
)

var (
// trustCenterStandardShortName is the short name of the trust center standard
// used to identify controls that should be flagged as trust center controls during clone
trustCenterStandardShortName = "OTS"

// trustCenterFrameworkName is the name of the framework the trust center standard belongs to, used in conjunction with trustCenterStandardShortName to identify controls that should be flagged as trust center controls during clone
trustCenterFrameworkName = "openlane-trust-center"
// TrustCenterFrameworkName is the name of the framework the trust center standard belongs to, used in conjunction with trustCenterStandardShortName to identify controls that should be flagged as trust center controls during clone
TrustCenterFrameworkName = "openlane-trust-center"

// StandardTrustCenterFrameworkName is the name of the fremework the default standards go to
StandardTrustCenterFrameworkName = "openlane-standard"

// trustCenterStandardFilter is the filter used to identify controls that should be flagged as trust center controls during clone
trustCenterStandardFilter = CloneFilterOptions{
StandardShortName: &trustCenterStandardShortName,
StandardFrameworkName: &trustCenterFrameworkName,
StandardFrameworkName: &TrustCenterFrameworkName,
}
)

Expand Down
2 changes: 1 addition & 1 deletion internal/ent/entityops/entity_registry.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

197 changes: 197 additions & 0 deletions internal/ent/hooks/listeners_onboarding_program.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package hooks

import (
"context"
"fmt"
"strings"
"time"

"entgo.io/ent/dialect/sql"
"github.com/theopenlane/iam/auth"

"github.com/theopenlane/core/common/enums"

"github.com/theopenlane/core/v2/internal/controls"
"github.com/theopenlane/core/v2/internal/ent/entityops"
"github.com/theopenlane/core/v2/internal/ent/generated"
"github.com/theopenlane/core/v2/internal/ent/generated/standard"
"github.com/theopenlane/core/v2/internal/workflows"
"github.com/theopenlane/core/v2/pkg/gala"
"github.com/theopenlane/core/v2/pkg/jsonx"
)

func init() { registerListeners(OnboardingProgramListeners) }

// OnboardingProgramListeners sets up gala to process onboarding requests such as creating programs
// and cloning them in the background
func OnboardingProgramListeners() []gala.Registration {
return []gala.Registration{
entityops.MutationListener{
Schema: entityops.SchemaOnboarding,
Operations: []string{entityops.OpCreate},
Caller: func(restored *auth.Caller, _ entityops.MutationPayload) *auth.Caller {
return restored.WithCapabilities(auth.CapInternalOperation)
},
Handle: handleOnboardingProgram,
},
}
}

func handleOnboardingProgram(inv entityops.Invocation, _ entityops.MutationPayload) error {
Comment thread
adelowo marked this conversation as resolved.
// cannot use MutationPayload because the organization_id needed is actually stored by the hook
// so it will not be available here
record, ok, err := entityops.LoadEntity(inv.Context, inv.EntityID, inv.Client.Onboarding.Get)
if err != nil || !ok {
return err
}

if len(record.Compliance) == 0 {
return nil
}

_, err = workflows.WithTx(inv.Context, inv.Client, nil, func(tx *generated.Tx) (struct{}, error) {
return struct{}{}, createProgram(inv.Context, tx.Client(), record.OrganizationID, record.Compliance)
})

return err
}

func generateProgramName(standards []*generated.Standard, year int) string {
if len(standards) == 1 {
name := standards[0].ShortName
if name == "" {
name = standards[0].Name
}

return fmt.Sprintf("%s Program %d", name, year)
}

return fmt.Sprintf("Compliance Program %d", year)
}

func createProgram(ctx context.Context, client *generated.Client, orgID string, complianceData map[string]interface{}) error {

Check failure on line 72 in internal/ent/hooks/listeners_onboarding_program.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=theopenlane_core&issues=AaCXTIm-rnQdoRHCKk27&open=AaCXTIm-rnQdoRHCKk27&pullRequest=2778
standards, labels, err := resolveOnboardingStandards(ctx, client, complianceData)
if err != nil || len(labels) == 0 {
return err
}

currentYear := time.Now().Year()

frameworks := strings.Join(labels, ", ")

description := fmt.Sprintf("Track %s compliance activities, evidence, and audit readiness for %d.", frameworks, currentYear)

builder := client.Program.Create().
SetOwnerID(orgID).
SetName(generateProgramName(standards, currentYear)).
SetDescription(description).
SetFrameworkName(frameworks)

if auditor, ok := complianceData["auditor_name"].(string); ok && auditor != "" {
builder.SetAuditor(auditor)
}

if email, ok := complianceData["auditor_email"].(string); ok && email != "" {
builder.SetAuditorEmail(email)
}

program, err := builder.Save(ctx)
if err != nil {
return err
}

for _, std := range standards {
filters := controls.CloneFilterOptions{StandardID: &std.ID}
if std.Framework == "soc2" {
filters.Categories = []string{"Security"}
}

where, err := controls.ControlFilterByStandard(ctx, filters, std)
if err != nil {
return err
}

sources, err := client.Control.Query().Where(where...).WithStandard().WithSubcontrols().All(ctx)
if err != nil {
return err
}

// can't use worker pool here since we are in a tx
for _, source := range sources {
input, _ := controls.CreateCloneControlInput(source, &program.ID, orgID)
cloned, err := client.Control.Create().SetInput(input).Save(ctx)
if err != nil {
return err
}

for _, subcontrol := range source.Edges.Subcontrols {
input := controls.CreateCloneSubcontrolInput(subcontrol, orgID, controls.SubcontrolToCreate{RefControl: source})
input.ControlID = cloned.ID
if err := client.Subcontrol.Create().SetInput(*input).Exec(ctx); err != nil {
return err
}
}
}
}

return nil
}

func getOnboardingFrameworks(complianceData map[string]interface{}) ([]string, error) {
var frameworks []string
if err := jsonx.RoundTrip(complianceData["frameworks"], &frameworks); err != nil {
return nil, fmt.Errorf("invalid onboarding frameworks: %w", err)
}

return frameworks, nil
}

func resolveOnboardingStandards(ctx context.Context, client *generated.Client, complianceData map[string]interface{}) ([]*generated.Standard, []string, error) {
frameworks, err := getOnboardingFrameworks(complianceData)
if err != nil || len(frameworks) == 0 {
return nil, nil, err
}

standards := make([]*generated.Standard, 0, len(frameworks))

labels := make([]string, 0, len(frameworks))
seen := make(map[string]bool, len(frameworks))

for _, framework := range frameworks {
if seen[framework] {
continue
}

seen[framework] = true

if framework == "other" {
labels = append(labels, "Other")
continue
}

std, err := client.Standard.Query().
Where(
standard.FrameworkEQ(framework),
standard.StatusEQ(enums.StandardActive),
standard.SystemOwned(true),
).
Order(
standard.ByVersion(sql.OrderDesc()),
standard.ByID(),
).
First(ctx)
if err != nil {
return nil, nil, fmt.Errorf("resolve onboarding framework %q: %w", framework, err)
}

standards = append(standards, std)
label := std.ShortName
if label == "" {
label = std.Name
}

labels = append(labels, label)
}

return standards, labels, nil
}
7 changes: 3 additions & 4 deletions internal/ent/hooks/listeners_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ import (
// functions in this package
var listenerProviders []func() []gala.Registration

// registerListeners adds a listener family constructor to the registry; call it from an
// init function in the file that declares the constructor
func registerListeners(provider func() []gala.Registration) {
listenerProviders = append(listenerProviders, provider)
// registerListeners takes in a variadic amount of listeners and sets them up
func registerListeners(providers ...func() []gala.Registration) {
listenerProviders = append(listenerProviders, providers...)
}

// AllListeners builds the registrations for every listener family in this package; the
Expand Down
56 changes: 4 additions & 52 deletions internal/ent/hooks/listeners_task_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"github.com/theopenlane/core/v2/internal/ent/generated"
"github.com/theopenlane/core/v2/internal/ent/generated/notification"
"github.com/theopenlane/core/v2/internal/ent/generated/organization"
"github.com/theopenlane/core/v2/internal/ent/generated/standard"
"github.com/theopenlane/core/v2/internal/ent/generated/task"
"github.com/theopenlane/core/v2/internal/ent/taskrules"
"github.com/theopenlane/core/v2/pkg/celx"
Expand Down Expand Up @@ -97,7 +96,7 @@ func handleTaskRuleMutation(inv entityops.Invocation, payload entityops.Mutation
continue
}

rendered, err := evaluateRule(inv.Context, inv.Client, fieldRule.Rule, value, placeholders)
rendered, err := evaluateRule(inv.Context, fieldRule.Rule, value, placeholders)
if err != nil {
logx.FromContext(inv.Context).Error().Err(err).Str("rule", fieldRule.Rule.RuleID).Msg("entityops: task rule evaluation failed")

Expand Down Expand Up @@ -234,10 +233,10 @@ type renderedTask struct {
}

// evaluateRule evaluates one rule against value: for EachElement rules it expands into one
// renderedTask per list element, resolving {label} via any registered resolver; for Expression
// renderedTask per list element; for Expression
// rules it fires at most one renderedTask when the condition is true. placeholders carries the
// firing entity's own fields (e.g. {id}, {body}), available regardless of which case fires
func evaluateRule(ctx context.Context, client *generated.Client, rule entityops.TaskRuleDescriptor, value any, placeholders map[string]string) ([]renderedTask, error) {
func evaluateRule(ctx context.Context, rule entityops.TaskRuleDescriptor, value any, placeholders map[string]string) ([]renderedTask, error) {
tmpl, ok := taskrules.Lookup(rule.RuleID)
if !ok {
return nil, fmt.Errorf("%w: %s", ErrMissingTaskTemplate, rule.RuleID)
Expand All @@ -264,9 +263,8 @@ func evaluateRule(ctx context.Context, client *generated.Client, rule entityops.

for _, element := range elements {
elementValue := fmt.Sprint(element)
label := resolveLabel(ctx, client, rule.RuleID, elementValue)

rendered, err := renderTask(tmpl, rule.RuleID, elementValue, label, placeholders)
rendered, err := renderTask(tmpl, rule.RuleID, elementValue, elementValue, placeholders)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -513,49 +511,3 @@ func evaluateCELList(ctx context.Context, expression string, value any) ([]any,

return list, nil
}

// TaskLabelResolver resolves a human-readable label for one EachElement value filling the {label} placeholder in a task template
type TaskLabelResolver func(ctx context.Context, client *generated.Client, value string) string

var taskLabelResolvers = map[string]TaskLabelResolver{
taskrules.RuleFramework: resolveFrameworkLabel,
}

// resolveLabel looks up value's label via any resolver registered for ruleID, falling back to
// value itself when none is registered or the resolver comes up empty
func resolveLabel(ctx context.Context, client *generated.Client, ruleID, value string) string {
resolver, ok := taskLabelResolvers[ruleID]
if !ok {
return value
}

if label := resolver(ctx, client, value); label != "" {
return label
}

return value
}

// resolveFrameworkLabel resolves a framework code (the value submitted for the "frameworks"
// onboarding question, see internal/onboarding/catalog.go's getFrameworkOptions) to its display name
func resolveFrameworkLabel(ctx context.Context, client *generated.Client, value string) string {
if client == nil {
return value
}

std, err := client.Standard.Query().
Where(
standard.FrameworkEQ(value),
standard.StatusEQ(enums.StandardActive),
).
First(ctx)
if err != nil {
return value
}

if std.ShortName != "" {
return std.ShortName
}

return std.Name
}
Loading