Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
23d3d2f
test(component): port Test_28 to upstream's unified user-overlay label
Apr 30, 2026
a83bb69
feat(cel): re-port CompareExecArgs hookup onto upstream's CP cache
Apr 30, 2026
eb0145d
feat(rules): R0040 'Unexpected process arguments' + Test_32 e2e
Apr 30, 2026
0816393
deps(storage): bump to rebased feat/exec-arg-wildcards tip (0de34ebc)
Apr 30, 2026
fbf98b0
ci(component-tests): add Test_32_UnexpectedProcessArguments to matrix
Apr 30, 2026
6f2a5b4
fix(containerprofilecache): re-wire R1016 tamper alert + expand Test_31
Apr 30, 2026
fe80d73
test(component): Test_32 profile uses full-path argv[0]
Apr 30, 2026
f828777
test: AP-fixture linter (R-AP-* rules) + canonical reference profile
Apr 30, 2026
7c10baa
fix(tamper_alert): accept self-signed profiles, only flag actual tamper
Apr 30, 2026
4d374d5
test(component): make Test_30 30b deterministic by re-execing inside …
Apr 30, 2026
a59e284
deps(storage): bump replace to f44fed80 (analyzer trailing-* fix)
May 1, 2026
bcf41ea
deps(storage): bump replace to 4ab95fb8 (PR #25 merged on fork main)
May 1, 2026
9385562
test(component): Test_33_AnalyzeOpensWildcardAnchoring
May 1, 2026
cb57674
test(component): rework Test_33 negative cases to probe under R0002's…
May 1, 2026
484d11c
test(component): fix Test_28 + Test_31 31b flakiness
May 1, 2026
4dd0b39
test(component): sign-after-roundtrip in Test_31 to defeat content-dr…
May 1, 2026
6dda020
test(component): bump Test_33 WaitForReady to 180s for cluster-pressu…
May 1, 2026
a5af261
deps(storage): bump replace to 43795bb4 (storage feat/exec-arg-wildca…
May 4, 2026
c4ac7b5
test(aplint): drop redundant p := p loop var (Go 1.22+, copyloopvar l…
May 4, 2026
e7dc486
fix(tamper_alert): R1016 dedup + use real WLID
May 4, 2026
98bdf97
deps(storage): bump replace to b0d68d3d (empty-Args wildcard match)
May 4, 2026
0cf4a50
fix: address CodeRabbit second-review batch on PR #38
May 9, 2026
8d89240
fix: address CodeRabbit third-review batch on PR #38 (0cf4a503)
May 9, 2026
26bc4bc
Merge remote-tracking branch 'origin/main' into merge/exec-arg-wildcards
May 9, 2026
045940c
fix(test): make dedup-clearing assertion non-trivial
May 9, 2026
eb40bbf
fix(ci): drop 'permissions: read-all' from reusable workflows
May 9, 2026
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
3 changes: 0 additions & 3 deletions .github/workflows/benchmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ on:
required: false
type: string

# Default to read-only at the workflow level (least privilege per Scorecard).
# Jobs that need elevated scopes override below.
permissions: read-all

concurrency:
group: benchmark-${{ github.ref }}
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/component-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,9 @@ jobs:
Test_28_UserDefinedNetworkNeighborhood,
Test_29_SignedApplicationProfile,
Test_30_TamperedSignedProfiles,
Test_31_TamperDetectionAlert
Test_31_TamperDetectionAlert,
Test_32_UnexpectedProcessArguments,
Test_33_AnalyzeOpensWildcardAnchoring
]
steps:
- name: Checkout code
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/go-basic-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ on:
GITGUARDIAN_API_KEY:
required: false

# Default to read-only at the workflow level (least privilege per Scorecard).
# Jobs that need elevated scopes override below.
permissions: read-all

jobs:
Check-secret:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/incluster-comp-pr-created.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ on:
GITGUARDIAN_API_KEY:
required: false

# Default to read-only at the workflow level (least privilege per Scorecard).
# Jobs that need elevated scopes override below.
permissions: read-all

jobs:
test:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/incluster-comp-pr-merged.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ on:
default: false
type: boolean

# Default to read-only at the workflow level (least privilege per Scorecard).
# Jobs that need elevated scopes override below.
permissions: read-all

jobs:
docker-build:
Expand Down
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ func main() {
ruleBindingCache.AddNotifier(&ruleBindingNotify)

cpc := containerprofilecache.NewContainerProfileCache(cfg, storageClient, k8sObjectCache, prometheusExporter)
// Wire R1016 tamper alerts: when a user-defined AP/NN overlay is
// loaded but its signature no longer verifies, the CP cache emits
// "Signed profile tampered" through this exporter. Optional —
// nil-safe inside the cache.
cpc.SetTamperAlertExporter(exporter)
cpc.Start(ctx)
logger.L().Info("ContainerProfileCache active; legacy AP/NN caches removed")

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -507,4 +507,4 @@ replace github.com/inspektor-gadget/inspektor-gadget => github.com/matthyx/inspe

replace github.com/cilium/ebpf => github.com/matthyx/ebpf v0.0.0-20260421101317-8a32d06def6c

replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05
replace github.com/kubescape/storage => github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -981,8 +981,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05 h1:RCEcduxCntYAuo8BleZu84Kk//X0gvsGrutQtdcLMn0=
github.com/k8sstormcenter/storage v0.0.240-0.20260429052903-0e0366026f05/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab h1:DNjKAs888GzW7P9gJUKtldL6E7zYzjLiO6pVUTvnzqc=
github.com/k8sstormcenter/storage v0.0.240-0.20260509184329-a7e6234349ab/go.mod h1:amdg/Qok9bqPzs1vZH5FW9/3MbCawc5wVsz9u3uIfu4=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
Expand Down
24 changes: 24 additions & 0 deletions pkg/objectcache/containerprofilecache/containerprofilecache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/kubescape/go-logger/helpers"
helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers"
"github.com/kubescape/node-agent/pkg/config"
"github.com/kubescape/node-agent/pkg/exporters"
"github.com/kubescape/node-agent/pkg/metricsmanager"
"github.com/kubescape/node-agent/pkg/objectcache"
"github.com/kubescape/node-agent/pkg/objectcache/callstackcache"
Expand Down Expand Up @@ -109,13 +110,26 @@ type ContainerProfileCacheImpl struct {
k8sObjectCache objectcache.K8sObjectCache
metricsManager metricsmanager.MetricsManager

// tamperAlertExporter receives R1016 "Signed profile tampered" alerts
// when a user-supplied AP/NN overlay fails signature verification. Set
// after construction via SetTamperAlertExporter; nil disables alerting.
tamperAlertExporter exporters.Exporter

reconcileEvery time.Duration
rpcBudget time.Duration
refreshInProgress atomic.Bool

// deprecationDedup tracks (kind|ns/name@rv) keys to emit one WARN log
// per legacy CRD resource-version across the process lifetime.
deprecationDedup sync.Map

// tamperEmitted dedup R1016 alerts: only emit once per
// (kind|ns/name@resourceVersion). Without this, the cache refresh loop
// would re-emit on every reconcile cycle, once per container reference.
// A re-tamper at a new resourceVersion still alerts because the key
// changes; verification passing again clears the entry so future
// transitions can re-alert.
tamperEmitted sync.Map
}

// NewContainerProfileCache creates a new ContainerProfileCacheImpl.
Expand Down Expand Up @@ -383,6 +397,12 @@ func (c *ContainerProfileCacheImpl) tryPopulateEntry(
helpers.Error(userAPErr))
userAP = nil
}
// Re-verify the user-supplied AP signature on every load. Emits
// R1016 if the profile is signed but tampered. Does not gate
// loading unless cfg.EnableSignatureVerification is true.
if userAP != nil && !c.verifyUserApplicationProfile(userAP, sharedData.Wlid) {
userAP = nil
}
var userNNErr error
_ = c.refreshRPC(ctx, func(rctx context.Context) error {
userNN, userNNErr = c.storageClient.GetNetworkNeighborhood(rctx, ns, overlayName)
Expand All @@ -396,6 +416,10 @@ func (c *ContainerProfileCacheImpl) tryPopulateEntry(
helpers.Error(userNNErr))
userNN = nil
}
// Same tamper-check on the NN side.
if userNN != nil && !c.verifyUserNetworkNeighborhood(userNN, sharedData.Wlid) {
userNN = nil
}
}

// Need SOMETHING to cache. If we have nothing, stay pending and retry.
Expand Down
190 changes: 190 additions & 0 deletions pkg/objectcache/containerprofilecache/tamper_alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Tamper detection for user-supplied profile overlays loaded into the
// ContainerProfileCache.
//
// When a user references a signed ApplicationProfile or NetworkNeighborhood
// via the `kubescape.io/user-defined-profile` pod label, this code path
// re-verifies the signature on every cache load and emits an R1016
// "Signed profile tampered" alert via the rule-alert exporter when the
// signature is present but no longer valid.
//
// This is the new home of the legacy applicationprofilecache's tamper
// detection (originally introduced in fork commit c2d681e0 — "Feat/
// tamperalert"). Upstream PR #788 deleted the legacy cache; this re-wires
// the same behavior onto containerprofilecache without changing the alert
// shape so existing component tests (Test_31_TamperDetectionAlert) keep
// working.
package containerprofilecache

import (
"errors"
"fmt"

"github.com/armosec/armoapi-go/armotypes"
"github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/node-agent/pkg/exporters"
"github.com/kubescape/node-agent/pkg/rulemanager/types"
"github.com/kubescape/node-agent/pkg/signature"
"github.com/kubescape/node-agent/pkg/signature/profiles"
"github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1"
)

// tamperKey uniquely identifies a tampered profile occurrence. ResourceVersion
// is included so that an attacker editing the resource (which changes RV) is
// re-flagged on the next reconcile cycle, while a long-lived broken profile
// only emits one R1016 across the cache's lifetime.
func tamperKey(kind, namespace, name, resourceVersion string) string {
return kind + "|" + namespace + "/" + name + "@" + resourceVersion
}

// SetTamperAlertExporter wires the rule-alert exporter used to emit R1016.
// Optional — when nil, signature verification still runs (and is logged)
// but no alert is emitted. Production wiring lives in cmd/main.go after the
// alert exporter is constructed.
func (c *ContainerProfileCacheImpl) SetTamperAlertExporter(e exporters.Exporter) {
c.tamperAlertExporter = e
}

// verifyUserApplicationProfile re-verifies the signature of a user-supplied
// ApplicationProfile overlay and emits R1016 if the signature is present
// but no longer valid (i.e. the profile was tampered after signing).
//
// Returns true iff the profile is acceptable for further use:
// - profile is signed and verifies → true
// - profile is not signed → true (signing is opt-in; the empty-signature
// case is handled by the caller's normal not-signed flow)
// - profile is signed but verification fails → false (and R1016 emitted)
//
// The boolean lets the caller decide whether to project the overlay into
// the cache. Today we always proceed (the legacy semantics don't actually
// gate loading on verification unless EnableSignatureVerification is true),
// but having the return value keeps the door open for stricter modes.
func (c *ContainerProfileCacheImpl) verifyUserApplicationProfile(profile *v1beta1.ApplicationProfile, wlid string) bool {
if profile == nil {
return true
}
adapter := profiles.NewApplicationProfileAdapter(profile)
if !signature.IsSigned(adapter) {
return true
}
key := tamperKey("ApplicationProfile", profile.Namespace, profile.Name, profile.ResourceVersion)
// AllowUntrusted: accept self-signed/local-CA signatures as long as the
// signature itself verifies against the cert in the annotations. We only
// want to flag actual tampering, not the absence of a Sigstore Fulcio
// trust chain. Matches `cmd/sign-object`'s default verifier.
err := signature.VerifyObjectAllowUntrusted(adapter)
if err == nil {
// Verified clean — clear any prior emit so future tampers re-alert.
c.tamperEmitted.Delete(key)
return true
}
// Classify the error: only ErrSignatureMismatch indicates an actual
// tamper event. Hash-computation, verifier-construction, and malformed-
// annotation errors are operational and MUST NOT raise R1016 — that
// would cause false alerts and, with EnableSignatureVerification=true,
// drop a valid overlay because of a transient operational failure.
if !errors.Is(err, signature.ErrSignatureMismatch) {
logger.L().Warning("user-defined ApplicationProfile signature verification operational error (NOT tamper)",
helpers.String("profile", profile.Name),
helpers.String("namespace", profile.Namespace),
helpers.String("wlid", wlid),
helpers.Error(err))
// Honour strict-mode: refuse to load on any verification failure,
// but do NOT touch the dedup map or emit R1016.
return !c.cfg.EnableSignatureVerification
}
// Real tamper.
logger.L().Warning("user-defined ApplicationProfile signature mismatch (tamper detected)",
helpers.String("profile", profile.Name),
helpers.String("namespace", profile.Namespace),
helpers.String("wlid", wlid),
helpers.Error(err))
// Dedup: emit R1016 only on first transition to invalid for this
// (kind, ns, name, resourceVersion). Otherwise the refresh loop would
// alert every reconcile cycle, once per container ref.
if _, alreadyEmitted := c.tamperEmitted.LoadOrStore(key, struct{}{}); !alreadyEmitted {
c.emitTamperAlert(profile.Name, profile.Namespace, wlid, "ApplicationProfile", err)
}
return !c.cfg.EnableSignatureVerification
}

// verifyUserNetworkNeighborhood is the NN-side counterpart to
// verifyUserApplicationProfile. Same contract, different object kind in
// the alert description.
func (c *ContainerProfileCacheImpl) verifyUserNetworkNeighborhood(nn *v1beta1.NetworkNeighborhood, wlid string) bool {
if nn == nil {
return true
}
adapter := profiles.NewNetworkNeighborhoodAdapter(nn)
if !signature.IsSigned(adapter) {
return true
}
key := tamperKey("NetworkNeighborhood", nn.Namespace, nn.Name, nn.ResourceVersion)
err := signature.VerifyObjectAllowUntrusted(adapter)
if err == nil {
c.tamperEmitted.Delete(key)
return true
}
// Same classification as the AP path — only ErrSignatureMismatch is a
// tamper; everything else is operational and must NOT trigger R1016.
if !errors.Is(err, signature.ErrSignatureMismatch) {
logger.L().Warning("user-defined NetworkNeighborhood signature verification operational error (NOT tamper)",
helpers.String("profile", nn.Name),
helpers.String("namespace", nn.Namespace),
helpers.String("wlid", wlid),
helpers.Error(err))
return !c.cfg.EnableSignatureVerification
}
logger.L().Warning("user-defined NetworkNeighborhood signature mismatch (tamper detected)",
helpers.String("profile", nn.Name),
helpers.String("namespace", nn.Namespace),
helpers.String("wlid", wlid),
helpers.Error(err))
if _, alreadyEmitted := c.tamperEmitted.LoadOrStore(key, struct{}{}); !alreadyEmitted {
c.emitTamperAlert(nn.Name, nn.Namespace, wlid, "NetworkNeighborhood", err)
}
return !c.cfg.EnableSignatureVerification
}

// emitTamperAlert sends a single R1016 "Signed profile tampered" alert
// through the rule-alert exporter. No-op when the exporter is unset.
//
// Alert shape mirrors the legacy applicationprofilecache.emitTamperAlert
// (fork commit c2d681e0) so dashboards and component tests keep matching.
// `wlid` should be the authoritative workload identifier the caller has on
// hand (e.g. sharedData.Wlid in containerprofilecache.go) — using the
// runtime containerID instead loses workload kind/name/cluster attribution
// because GenericRuleFailure.SetWorkloadDetails() parses it as a WLID.
func (c *ContainerProfileCacheImpl) emitTamperAlert(profileName, namespace, wlid, objectKind string, verifyErr error) {
if c.tamperAlertExporter == nil {
return
}

ruleFailure := &types.GenericRuleFailure{
BaseRuntimeAlert: armotypes.BaseRuntimeAlert{
AlertName: "Signed profile tampered",
InfectedPID: 1,
Severity: 10,
FixSuggestions: "Investigate who modified the " + objectKind + " '" + profileName + "' in namespace '" + namespace + "'. Re-sign the profile after verifying its contents.",
},
AlertType: armotypes.AlertTypeRule,
RuntimeProcessDetails: armotypes.ProcessTree{
ProcessTree: armotypes.Process{
PID: 1,
Comm: "node-agent",
},
},
RuleAlert: armotypes.RuleAlert{
RuleDescription: fmt.Sprintf("Signed %s '%s' in namespace '%s' has been tampered with: %v",
objectKind, profileName, namespace, verifyErr),
},
RuntimeAlertK8sDetails: armotypes.RuntimeAlertK8sDetails{
Namespace: namespace,
},
RuleID: "R1016",
}

ruleFailure.SetWorkloadDetails(wlid)

c.tamperAlertExporter.SendRuleAlert(ruleFailure)
}
Loading
Loading