Skip to content

Commit 29bbdff

Browse files
authored
feat: risk worker redesign (#360)
* feat: risk worker redesign Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * fix: small performance tuning Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * feat: resolve failed evidence Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * fix: copilot issues Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> * Fix Copilot review comments: optimize preloads, batch-load templates, return errors for unknown operators * fix: risk transition Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com> --------- Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>
1 parent 8c230b7 commit 29bbdff

17 files changed

Lines changed: 1711 additions & 413 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ CCF_ENVIRONMENT="" # Defaults to production.
1616
## This configuration disables cookie setting to allow testing on Safari
1717
## It is insecure so use it with caution
1818
#CCF_ENVIRONMENT="local"
19+
20+
# Pprof debugging (disabled by default)
21+
# Enable this for profiling during load tests
22+
CCF_PPROF_ENABLED=false
23+
CCF_PPROF_PORT=6060

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ func setDefaultEnvironmentVariables() {
2929
viper.SetDefault("evidence_default_expiry_months", "1")
3030
viper.SetDefault("digest_enabled", "true")
3131
viper.SetDefault("digest_schedule", "@weekly")
32+
viper.SetDefault("pprof_enabled", "false")
33+
viper.SetDefault("pprof_port", ":6060")
3234
}
3335

3436
func bindEnvironmentVariables() {
@@ -53,6 +55,8 @@ func bindEnvironmentVariables() {
5355
viper.MustBindEnv("evidence_default_expiry_months")
5456
viper.MustBindEnv("digest_enabled")
5557
viper.MustBindEnv("digest_schedule")
58+
viper.MustBindEnv("pprof_enabled")
59+
viper.MustBindEnv("pprof_port")
5660
}
5761

5862
func init() {

cmd/run.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cmd
33
import (
44
"context"
55
"log"
6+
"net/http"
7+
_ "net/http/pprof"
68

79
"github.com/compliance-framework/api/internal/api"
810
"github.com/compliance-framework/api/internal/api/handler"
@@ -155,6 +157,15 @@ func RunServer(cmd *cobra.Command, args []string) {
155157
metrics.StartMetricsServer(cfg.MetricsPort)
156158
}
157159

160+
if cfg.PprofEnabled {
161+
sugar.Infow("Starting pprof server", "port", cfg.PprofPort)
162+
go func() {
163+
if err := http.ListenAndServe(cfg.PprofPort, nil); err != nil {
164+
sugar.Errorw("Failed to start pprof server", "error", err)
165+
}
166+
}()
167+
}
168+
158169
if err := server.Start(cfg.AppPort); err != nil {
159170
sugar.Fatalw("Failed to start server", "error", err)
160171
}

internal/api/handler/risks.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,10 @@ func validateStatusTransition(oldStatus, newStatus string) error {
19931993
string(riskrel.RiskStatusRiskAccepted): {
19941994
string(riskrel.RiskStatusClosed): {},
19951995
},
1996+
string(riskrel.RiskStatusRemediated): {
1997+
string(riskrel.RiskStatusOpen): {},
1998+
string(riskrel.RiskStatusClosed): {},
1999+
},
19962000
string(riskrel.RiskStatusClosed): {},
19972001
}
19982002

internal/config/config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ type Config struct {
3939
DigestSchedule string // Cron schedule for digest emails
4040
Workflow *WorkflowConfig
4141
Risk *RiskConfig
42+
PprofEnabled bool // Enable or disable pprof debugging server
43+
PprofPort string // Port for pprof debugging server
4244
}
4345

4446
func NewConfig(logger *zap.SugaredLogger) *Config {
@@ -197,6 +199,17 @@ func NewConfig(logger *zap.SugaredLogger) *Config {
197199
workerConfig.Queue = viper.GetString("worker_queue")
198200
}
199201

202+
pprofEnabled := viper.GetBool("pprof_enabled")
203+
pprofPort := viper.GetString("pprof_port")
204+
if pprofPort == "" {
205+
pprofPort = "6060"
206+
}
207+
// Normalize pprofPort: if it's a bare port like "6060", prefix with ":" to form ":6060".
208+
// If it already contains a colon (e.g. "127.0.0.1:6060"), leave it unchanged.
209+
if !strings.Contains(pprofPort, ":") {
210+
pprofPort = ":" + pprofPort
211+
}
212+
200213
return &Config{
201214
AppPort: appPort,
202215
Environment: environment,
@@ -218,6 +231,8 @@ func NewConfig(logger *zap.SugaredLogger) *Config {
218231
DigestSchedule: digestSchedule,
219232
Workflow: workflowConfig,
220233
Risk: riskConfig,
234+
PprofEnabled: pprofEnabled,
235+
PprofPort: pprofPort,
221236
}
222237

223238
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package labelfilter
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
)
8+
9+
// NormalizeLabels converts a slice of name/value label pairs into a map keyed
10+
// by lowercased label name, with each value being a slice of lowercased values.
11+
// This supports labels where a single name can have multiple values (e.g. multiple
12+
// "_policy" labels on the same evidence).
13+
func NormalizeLabels(labels []struct{ Name, Value string }) map[string][]string {
14+
result := make(map[string][]string, len(labels))
15+
for _, l := range labels {
16+
key := strings.ToLower(strings.TrimSpace(l.Name))
17+
val := strings.ToLower(strings.TrimSpace(l.Value))
18+
if key == "" {
19+
continue
20+
}
21+
result[key] = append(result[key], val)
22+
}
23+
return result
24+
}
25+
26+
// MatchLabels evaluates a filter Scope against a normalized label map.
27+
// A nil scope matches everything (empty filter = match all).
28+
// Semantics mirror the SQL evaluator in evidence.go (case-insensitive via pre-normalized map).
29+
// Returns an error if an unknown query operator is encountered.
30+
func MatchLabels(scope *Scope, labels map[string][]string) (bool, error) {
31+
if scope == nil {
32+
return true, nil
33+
}
34+
return matchScope(*scope, labels)
35+
}
36+
37+
func matchScope(scope Scope, labels map[string][]string) (bool, error) {
38+
if scope.IsCondition() {
39+
return matchCondition(*scope.Condition, labels), nil
40+
}
41+
if scope.IsQuery() {
42+
return matchQuery(*scope.Query, labels)
43+
}
44+
// Empty scope (neither condition nor query) matches everything.
45+
return true, nil
46+
}
47+
48+
func matchCondition(cond Condition, labels map[string][]string) bool {
49+
key := strings.ToLower(strings.TrimSpace(cond.Label))
50+
val := strings.ToLower(strings.TrimSpace(cond.Value))
51+
52+
values, exists := labels[key]
53+
54+
switch cond.Operator {
55+
case "!=":
56+
// Not-equals: true if the label doesn't exist or none of its values match.
57+
if !exists {
58+
return true
59+
}
60+
if slices.Contains(values, val) {
61+
return false
62+
}
63+
return true
64+
default:
65+
// Equals (default): true if any value for this label matches.
66+
if !exists {
67+
return false
68+
}
69+
return slices.Contains(values, val)
70+
}
71+
}
72+
73+
func matchQuery(query Query, labels map[string][]string) (bool, error) {
74+
op := strings.ToLower(query.Operator)
75+
switch op {
76+
case "and":
77+
for _, scope := range query.Scopes {
78+
match, err := matchScope(scope, labels)
79+
if err != nil {
80+
return false, err
81+
}
82+
if !match {
83+
return false, nil
84+
}
85+
}
86+
return true, nil
87+
case "or":
88+
for _, scope := range query.Scopes {
89+
match, err := matchScope(scope, labels)
90+
if err != nil {
91+
return false, err
92+
}
93+
if match {
94+
return true, nil
95+
}
96+
}
97+
return len(query.Scopes) == 0, nil
98+
default:
99+
return false, fmt.Errorf("unrecognised query operator: %s", query.Operator)
100+
}
101+
}

0 commit comments

Comments
 (0)