Skip to content

Commit 2f022dd

Browse files
committed
feat(examples): add per-asset disabled queries to worker example
Allow specific policy checks to be skipped for individual assets by fingerprint, enabling accepted-risk workflows without modifying policies.
1 parent e57a982 commit 2f022dd

2 files changed

Lines changed: 152 additions & 12 deletions

File tree

_examples/worker/main.go

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,63 @@ import (
4040
_ "github.com/kopexa-grc/kspec/provider/all"
4141
)
4242

43+
// ---------------------------------------------------------------------------
44+
// Disabled queries (per asset)
45+
// ---------------------------------------------------------------------------
46+
47+
// disabledQueries maps asset fingerprints to query UIDs that should be skipped.
48+
// In production, this would come from a database or API.
49+
var disabledQueries = map[string][]string{
50+
AssetFingerprint("network", "host", "example.com"): {"tls-cert-not-expiring"},
51+
AssetFingerprint("network", "host", "cloudflare.com"): {"tls-modern-version", "tls-cert-not-expiring"},
52+
}
53+
54+
// filterDisabledQueries returns a deep copy of the policies with checks matching
55+
// the disabled UIDs removed from both Groups[].Checks and top-level Queries.
56+
// The original slice is not modified so it can be reused across integrations.
57+
func filterDisabledQueries(policies []policy.Policy, disabled []string) []policy.Policy {
58+
if len(disabled) == 0 {
59+
return policies
60+
}
61+
62+
set := make(map[string]struct{}, len(disabled))
63+
for _, uid := range disabled {
64+
set[uid] = struct{}{}
65+
}
66+
67+
out := make([]policy.Policy, len(policies))
68+
copy(out, policies)
69+
70+
for i, p := range out {
71+
// Filter groups and their checks.
72+
var groups []policy.Group
73+
for _, g := range p.Groups {
74+
var checks []policy.Check
75+
for _, c := range g.Checks {
76+
if _, skip := set[c.UID]; !skip {
77+
checks = append(checks, c)
78+
}
79+
}
80+
if len(checks) > 0 {
81+
g.Checks = checks
82+
groups = append(groups, g)
83+
}
84+
}
85+
out[i].Groups = groups
86+
87+
// Filter top-level queries.
88+
var queries []policy.Check
89+
for _, q := range p.Queries {
90+
if _, skip := set[q.UID]; !skip {
91+
queries = append(queries, q)
92+
}
93+
}
94+
out[i].Queries = queries
95+
}
96+
97+
return out
98+
}
99+
43100
// ---------------------------------------------------------------------------
44101
// Asset Store
45102
// ---------------------------------------------------------------------------
@@ -136,13 +193,14 @@ type IntegrationResult struct {
136193
ResourceTypes int
137194
TotalResources int
138195
// Scan
139-
Score uint32
140-
Grade string
141-
RiskLevel string
142-
Findings scoring.Findings
143-
Duration time.Duration
144-
Phase string // "discovery", "sync", "scan", "complete"
145-
Err error
196+
Score uint32
197+
Grade string
198+
RiskLevel string
199+
Findings scoring.Findings
200+
DisabledCount int // Number of queries disabled for this asset
201+
Duration time.Duration
202+
Phase string // "discovery", "sync", "scan", "complete"
203+
Err error
146204
}
147205

148206
// ---------------------------------------------------------------------------
@@ -309,6 +367,15 @@ func processIntegration(ctx context.Context, store AssetStore, integration Integ
309367
return result
310368
}
311369

370+
// Apply per-asset disabled queries.
371+
if disabled, ok := disabledQueries[fingerprint]; ok {
372+
policies = filterDisabledQueries(policies, disabled)
373+
result.DisabledCount = len(disabled)
374+
375+
fmt.Fprintf(os.Stderr, "[%s] Disabled %d queries for asset %s: %v\n",
376+
integration.ID, len(disabled), integration.AssetName, disabled)
377+
}
378+
312379
s := scanner.NewScanner(scanner.ScanConfig{
313380
ProviderName: integration.Provider,
314381
ProviderConfig: integration.Config,
@@ -433,8 +500,8 @@ func main() {
433500
fmt.Println("=== Results ===")
434501
fmt.Println()
435502

436-
header := fmt.Sprintf("%-34s %-18s %6s %6s %-10s %4s %4s %4s %4s %10s %-10s",
437-
"Fingerprint", "Asset", "ResTyp", "ResAll", "Grade", "Crit", "High", "Med", "Low", "Duration", "Phase")
503+
header := fmt.Sprintf("%-34s %-18s %6s %6s %-10s %4s %4s %4s %4s %4s %10s %-10s",
504+
"Fingerprint", "Asset", "ResTyp", "ResAll", "Grade", "Skip", "Crit", "High", "Med", "Low", "Duration", "Phase")
438505
fmt.Println(header)
439506
fmt.Println(strings.Repeat("-", len(header)))
440507

@@ -444,10 +511,10 @@ func main() {
444511
if r.Err != nil {
445512
failed++
446513

447-
fmt.Printf("%-34s %-18s %6s %6s %-10s %4s %4s %4s %4s %10s %-10s ERR: %v\n",
514+
fmt.Printf("%-34s %-18s %6s %6s %-10s %4s %4s %4s %4s %4s %10s %-10s ERR: %v\n",
448515
truncate(r.Fingerprint, 34),
449516
truncate(r.Asset, 18),
450-
"-", "-", "-", "-", "-", "-", "-",
517+
"-", "-", "-", "-", "-", "-", "-", "-",
451518
r.Duration.Truncate(time.Millisecond),
452519
r.Phase,
453520
r.Err,
@@ -458,12 +525,13 @@ func main() {
458525

459526
succeeded++
460527

461-
fmt.Printf("%-34s %-18s %6d %6d %-10s %4d %4d %4d %4d %10s %-10s\n",
528+
fmt.Printf("%-34s %-18s %6d %6d %-10s %4d %4d %4d %4d %4d %10s %-10s\n",
462529
truncate(r.Fingerprint, 34),
463530
truncate(r.Asset, 18),
464531
r.ResourceTypes,
465532
r.TotalResources,
466533
fmt.Sprintf("%s (%d)", r.Grade, r.Score),
534+
r.DisabledCount,
467535
r.Findings.Critical,
468536
r.Findings.High,
469537
r.Findings.Medium,

docs/integration.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,78 @@ Each worker runs the full pipeline (discover → sync → scan → update) for o
467467

468468
See [`_examples/worker/`](../_examples/worker/) for a complete working example.
469469

470+
### 4.5 Disabling Queries Per Asset
471+
472+
Some assets may have accepted risks — for example, a staging server with an expiring certificate that does not need the `tls-cert-not-expiring` check. kspec supports disabling specific policy checks per asset **at the application level**, without modifying policies or kspec internals.
473+
474+
**Pattern: external map of fingerprint → disabled UIDs**
475+
476+
Maintain a mapping from asset fingerprints to the query UIDs that should be skipped:
477+
478+
```go
479+
// In production, load from a database or API.
480+
var disabledQueries = map[string][]string{
481+
AssetFingerprint("network", "host", "example.com"): {"tls-cert-not-expiring"},
482+
AssetFingerprint("network", "host", "cloudflare.com"): {"tls-modern-version", "tls-cert-not-expiring"},
483+
}
484+
```
485+
486+
**Approach: copy and strip**
487+
488+
Before passing policies to the scanner, deep-copy them and remove any checks whose UID appears in the disabled set:
489+
490+
```go
491+
func filterDisabledQueries(policies []policy.Policy, disabled []string) []policy.Policy {
492+
set := make(map[string]struct{}, len(disabled))
493+
for _, uid := range disabled {
494+
set[uid] = struct{}{}
495+
}
496+
497+
out := make([]policy.Policy, len(policies))
498+
copy(out, policies)
499+
500+
for i, p := range out {
501+
// Filter groups — remove matching checks, drop empty groups.
502+
var groups []policy.Group
503+
for _, g := range p.Groups {
504+
var checks []policy.Check
505+
for _, c := range g.Checks {
506+
if _, skip := set[c.UID]; !skip {
507+
checks = append(checks, c)
508+
}
509+
}
510+
if len(checks) > 0 {
511+
g.Checks = checks
512+
groups = append(groups, g)
513+
}
514+
}
515+
out[i].Groups = groups
516+
517+
// Filter top-level queries the same way.
518+
var queries []policy.Check
519+
for _, q := range p.Queries {
520+
if _, skip := set[q.UID]; !skip {
521+
queries = append(queries, q)
522+
}
523+
}
524+
out[i].Queries = queries
525+
}
526+
527+
return out
528+
}
529+
```
530+
531+
The original policy slice is never mutated, so it can be reused across integrations.
532+
533+
**Production notes:**
534+
535+
- **Storage** — Store disabled query mappings in a database table (e.g., `asset_disabled_queries`) keyed by asset fingerprint and query UID
536+
- **Admin API** — Expose endpoints for security teams to disable/re-enable queries per asset
537+
- **Audit log** — Record who disabled a query, when, and the justification for compliance traceability
538+
- **Expiry** — Consider adding an expiration date so accepted risks are automatically re-evaluated
539+
540+
See [`_examples/worker/`](../_examples/worker/) for a complete working example with disabled queries.
541+
470542
## Part 5: Provider Registration
471543

472544
kspec uses Go's `init()` pattern for provider registration. Every program that uses kspec must import at least one provider package:

0 commit comments

Comments
 (0)