Repository: kubernetes-sigs/security-profiles-operator
Pull request #338 (2021-03-03 15:34:40)

Add leases RBAC to webhook
#### What type of PR is this?

/kind regression


#### What this PR does / why we need it:
This adds necessary leader election RBAC permissions to the webhooks.
#### Which issue(s) this PR fixes:

<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.

Fixes #

or

None
-->
None
#### Does this PR have test?
None
<!--
If tests aren't applicable just write N/A.
-->

#### Special notes for your reviewer:
None
#### Does this PR introduce a user-facing change?

<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

```release-note
None
```


k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/security-profiles-operator/pull/338#" title="Author self-approved">saschagrunert</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fsecurity-profiles-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/security-profiles-operator/blob/master/OWNERS)~~ [saschagrunert]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/deploy/base/webhook/role.yaml b/deploy/base/webhook/role.yaml
index e492bd7e6..1ba2c8cee 100644
--- a/deploy/base/webhook/role.yaml
+++ b/deploy/base/webhook/role.yaml
@@ -6,6 +6,17 @@ metadata:
   creationTimestamp: null
   name: spo-webhook
 rules:
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -87,11 +98,18 @@ metadata:
   name: spo-webhook
   namespace: security-profiles-operator
 rules:
+- apiGroups:
+  - coordination.k8s.io
+  resources:
+  - leases
+  verbs:
+  - create
+  - get
+  - update
 - apiGroups:
   - ""
   resources:
   - configmaps
-  - events
   verbs:
   - create
   - get
diff --git a/deploy/webhook.yaml b/deploy/webhook.yaml
index b3a56e275..88c8ebec6 100644
--- a/deploy/webhook.yaml
+++ b/deploy/webhook.yaml
@@ -188,6 +188,17 @@ metadata:
     app: security-profiles-operator
   name: spo-webhook
 rules:
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - get
+  - list
+  - patch
+  - update
+  - watch
 - apiGroups:
   - ""
   resources:
@@ -270,11 +281,18 @@ metadata:
   name: spo-webhook
   namespace: security-profiles-operator
 rules:
+- apiGroups:
+  - coordination.k8s.io
+  resources:
+  - leases
+  verbs:
+  - create
+  - get
+  - update
 - apiGroups:
   - ""
   resources:
   - configmaps
-  - events
   verbs:
   - create
   - get
diff --git a/doc/user-stories.md b/doc/user-stories.md
index 5678dc78d..ee07e251f 100644
--- a/doc/user-stories.md
+++ b/doc/user-stories.md
@@ -30,6 +30,16 @@ in a certain profile. E.g. for SELinux a missing parenthesis or something of the
 sort… This is considered for SELinux in
 [an issue](https://github.com/kubernetes-sigs/security-profiles-operator/issues/223)
 
+### As an application developer, Ming would like to be able to automatically generate initial security profiles that are specific to the application.
+
+Details:
+
+* For improved profiles, users could define a process to be triggered during the profiling process (i.e. execution of E2E tests).
+* Avoid potential knowledge short falls by priming essential requirements 
+(e.g. blocked essential syscalls may cause 
+[issues](https://github.com/kubernetes/kubernetes/issues/85191) in seccomp)
+* The initial profile will need to be "manually" refined over time.
+
 ### As an application SRE, Rajesh needs to be able to see the state of the installed profile(s) and verify that it has indeed been installed on the system.
 
 ### As an application SRE, Rajesh needs to be able to do long runs of a security profile in “complain-mode” to easily identify the impact of a profile being enforced without impacting the application.
diff --git a/go.mod b/go.mod
index 6b2c8aff1..1df112e81 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module sigs.k8s.io/security-profiles-operator
 go 1.15
 
 require (
-	github.com/cirocosta/dmesg_exporter v0.0.0-20190515130104-6e3e42fc8d7b
 	github.com/containers/common v0.35.2
 	github.com/crossplane/crossplane-runtime v0.13.0
 	github.com/go-logr/logr v0.4.0
@@ -14,7 +13,7 @@ require (
 	k8s.io/api v0.20.4
 	k8s.io/apimachinery v0.20.4
 	k8s.io/client-go v0.20.4
-	k8s.io/klog/v2 v2.5.0
+	k8s.io/klog/v2 v2.6.0
 	k8s.io/release v0.7.0
 	sigs.k8s.io/controller-runtime v0.8.2
 	sigs.k8s.io/controller-tools v0.5.0
diff --git a/go.sum b/go.sum
index 440914aa4..d0856e433 100644
--- a/go.sum
+++ b/go.sum
@@ -139,8 +139,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
 github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
 github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s=
 github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
-github.com/cirocosta/dmesg_exporter v0.0.0-20190515130104-6e3e42fc8d7b h1:jk36mztUq5PHmR1J6H5FQg/vur4rrAowTuCPFamr5wQ=
-github.com/cirocosta/dmesg_exporter v0.0.0-20190515130104-6e3e42fc8d7b/go.mod h1:WZ6urbJg8HKOXim+GvWEB2mm2qhOpXWb8ILiIYjYyns=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -637,7 +635,6 @@ github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
 github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
 github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -687,7 +684,6 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/pquerna/ffjson v0.0.0-20181028064349-e517b90714f7/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
 github.com/pquerna/ffjson v0.0.0-20190813045741-dac163c6c0a9/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
-github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@@ -700,7 +696,6 @@ github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -709,7 +704,6 @@ github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+
 github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@@ -1029,7 +1023,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180920110915-d641721ec2de/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1412,8 +1405,8 @@ k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
-k8s.io/klog/v2 v2.5.0 h1:8mOnjf1RmUPW6KRqQCfYSZq/K20Unmp3IhuZUhxl8KI=
-k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
+k8s.io/klog/v2 v2.6.0 h1:c1wFxejFMBkp/VxCdc6kYdgrBkC2gzmcl6afuJAkJyU=
+k8s.io/klog/v2 v2.6.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
diff --git a/internal/pkg/daemon/enricher/audit.go b/internal/pkg/daemon/enricher/audit.go
index c4c1e58e5..43266bed1 100644
--- a/internal/pkg/daemon/enricher/audit.go
+++ b/internal/pkg/daemon/enricher/audit.go
@@ -23,37 +23,74 @@ import (
 	"github.com/pkg/errors"
 )
 
-// Initially only seccomp logs  are supported
 // type IDs are defined at https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/audit.h
-var auditRegex = regexp.MustCompile(`audit:.+type=1326.+audit\((.+)\).+pid=(\b\d+\b).+exe="(.+)".+syscall=(\b\d+\b).*`)
+var (
+	//nolint:lll
+	seccompLineRegex = regexp.MustCompile(`(type=SECCOMP|audit:.+type=1326).+audit\((.+)\).+pid=(\b\d+\b).+exe="(.+)".+syscall=(\b\d+\b).*`)
+	selinuxLineRegex = regexp.MustCompile(`type=AVC.+audit\((.+)\).+pid=(\b\d+\b).*`)
+)
 
-// minimum numbers of captures expected on a supported log line.
-var minCapturesExpected = 5
+var (
+	minSeccompCapturesExpected = 5
+	minSelinuxCapturesExpected = 3
+)
 
 // isAuditLine checks whether logLine is a supported audit line.
 func isAuditLine(logLine string) bool {
-	captures := auditRegex.FindStringSubmatch(logLine)
+	captures := seccompLineRegex.FindStringSubmatch(logLine)
+	if len(captures) >= minSeccompCapturesExpected {
+		return true
+	}
 
-	return len(captures) > 1
+	captures = selinuxLineRegex.FindStringSubmatch(logLine)
+	return len(captures) >= minSelinuxCapturesExpected
 }
 
 // extractAuditLine extracts an auditline from logLine.
 func extractAuditLine(logLine string) (*auditLine, error) {
-	captures := auditRegex.FindStringSubmatch(logLine)
-	if len(captures) < minCapturesExpected {
-		return nil, errors.Wrap(errUnsupportedLogLine, logLine)
+	if seccomp := extractSeccompLine(logLine); seccomp != nil {
+		return seccomp, nil
+	}
+
+	if selinux := extractSelinuxLine(logLine); selinux != nil {
+		return selinux, nil
+	}
+
+	return nil, errors.Wrap(errUnsupportedLogLine, logLine)
+}
+
+func extractSeccompLine(logLine string) *auditLine {
+	captures := seccompLineRegex.FindStringSubmatch(logLine)
+	if len(captures) < minSeccompCapturesExpected {
+		return nil
 	}
 
 	line := auditLine{}
 	line.Type = "seccomp"
-	line.TimestampID = captures[1]
-	line.Executable = captures[3]
-	if v, err := strconv.Atoi(captures[2]); err == nil {
+	line.TimestampID = captures[2]
+	line.Executable = captures[4]
+	if v, err := strconv.Atoi(captures[3]); err == nil {
 		line.ProcessID = v
 	}
-	if v, err := strconv.Atoi(captures[4]); err == nil {
+	if v, err := strconv.Atoi(captures[5]); err == nil {
 		line.SystemCallID = v
 	}
 
-	return &line, nil
+	return &line
+}
+
+func extractSelinuxLine(logLine string) *auditLine {
+	captures := selinuxLineRegex.FindStringSubmatch(logLine)
+	if len(captures) < minSelinuxCapturesExpected {
+		return nil
+	}
+
+	line := auditLine{}
+	line.Type = "selinux"
+	line.TimestampID = captures[1]
+	if v, err := strconv.Atoi(captures[2]); err == nil {
+		line.ProcessID = v
+	}
+
+	return &line
 }
diff --git a/internal/pkg/daemon/enricher/container.go b/internal/pkg/daemon/enricher/container.go
index 1ad7bed43..b44c9c60d 100644
--- a/internal/pkg/daemon/enricher/container.go
+++ b/internal/pkg/daemon/enricher/container.go
@@ -17,8 +17,12 @@ limitations under the License.
 package enricher
 
 import (
+	"bufio"
 	"context"
 	"fmt"
+	"os"
+	"path/filepath"
+	"regexp"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -28,7 +32,8 @@ import (
 )
 
 const (
-	crioPrefix = "cri-o://"
+	crioCgroupRegex = `\/.+-([a-f0-9]+)`
+	crioPrefix      = "cri-o://"
 )
 
 // NOTE(jaosorior): Should this actually be namespace-scoped?
@@ -80,3 +85,38 @@ func containerIDRaw(containerID string) (string, error) {
 
 	return "", errors.Wrap(errUnsupportedContainerRuntime, containerID)
 }
+
+func getContainerID(processID int) string {
+	cgroupFile := fmt.Sprintf("/proc/%d/cgroup", processID)
+	file, err := os.Open(filepath.Clean(cgroupFile))
+	if err != nil {
+		logger.Error(nil, "could not open cgroup", "process-id", processID)
+		return ""
+	}
+	defer func() {
+		cerr := file.Close()
+		if err == nil {
+			err = cerr
+		}
+	}()
+
+	scanner := bufio.NewScanner(file)
+	scanner.Split(bufio.ScanLines)
+	for scanner.Scan() {
+		if containerID := extractID(scanner.Text()); containerID != "" {
+			return containerID
+		}
+	}
+
+	return ""
+}
+
+func extractID(cgroup string) string {
+	containerIDRegex := regexp.MustCompile(crioCgroupRegex)
+	capture := containerIDRegex.FindStringSubmatch(cgroup)
+	if len(capture) > 1 {
+		return capture[1]
+	}
+
+	return ""
+}
diff --git a/internal/pkg/daemon/enricher/main.go b/internal/pkg/daemon/enricher/main.go
index 6cb83d6d7..e321dde69 100644
--- a/internal/pkg/daemon/enricher/main.go
+++ b/internal/pkg/daemon/enricher/main.go
@@ -17,23 +17,18 @@ limitations under the License.
 package enricher
 
 import (
-	"bufio"
-	"context"
 	"fmt"
 	"io"
 	"os"
-	"os/signal"
 	"path/filepath"
-	"regexp"
-	"syscall"
+	"time"
 
-	"github.com/cirocosta/dmesg_exporter/kmsg"
-	"github.com/cirocosta/dmesg_exporter/reader"
 	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
 )
 
 var (
-	logFile = "/dev/kmsg"
+	logFile = "/var/log/spo.log"
 	logger  logr.Logger
 )
 
@@ -50,10 +45,17 @@ func Run(l logr.Logger) {
 	logger.V(1).Info("starting log-exporter on node: ", nodeName)
 
 	auditLines := make(chan string)
-	go tailDevice(logFile, auditLines)
+	tailErrors := make(chan error)
+	go tailFile(logFile, auditLines, tailErrors)
 
 	for {
-		line := <-auditLines
+		var line string
+		select {
+		case err := <-tailErrors:
+			logger.Error(err, "tail audit log")
+			os.Exit(1)
+		case line = <-auditLines:
+		}
 
 		if !isAuditLine(line) {
 			continue
@@ -85,84 +87,38 @@ func Run(l logr.Logger) {
 	}
 }
 
-func blockAndCancelOnSignal(cancel context.CancelFunc) {
-	sigChan := make(chan os.Signal, 1)
-	signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
-	<-sigChan
-
-	cancel()
-}
-
-func tailDevice(device string, msgs chan string) {
-	file, err := os.Open(device)
+func tailFile(filePath string, lines chan string, errChan chan error) {
+	file, err := os.Open(filepath.Clean(filePath))
 	if err != nil {
+		errChan <- errors.Wrap(err, "open audit log file")
 		return
 	}
-	defer file.Close()
+	defer func() {
+		err := file.Close()
+		if err != nil {
+			errChan <- errors.Wrap(err, "close audit log file")
+		}
+	}()
 
-	// seek to the end of device
-	_, err = file.Seek(0, io.SeekEnd)
+	offset, err := file.Seek(0, io.SeekEnd)
 	if err != nil {
+		errChan <- errors.Wrap(err, "seek end audit log")
 		return
 	}
 
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-
-	go blockAndCancelOnSignal(cancel)
-
-	var (
-		r        = reader.NewReader(file)
-		messages = make(chan *kmsg.Message, 1)
-	)
-
-	kmsgErrorsChan := r.Listen(ctx, messages)
-
+	buffer := make([]byte, 1024)
 	for {
-		select {
-		case <-kmsgErrorsChan:
-			return
-		case <-ctx.Done():
-			return
-		case message := <-messages:
-			if message == nil {
+		readBytes, err := file.ReadAt(buffer, offset)
+		if err != nil {
+			if !errors.Is(err, io.EOF) {
+				errChan <- errors.Wrap(err, "read audit log buffer")
 				return
 			}
-
-			if message.Facility != kmsg.FacilityKern {
-				continue
-			}
-
-			msgs <- message.Message
-		}
-	}
-}
-
-func getContainerID(processID int) string {
-	cgroupFile := fmt.Sprintf("/proc/%d/cgroup", processID)
-	file, err := os.Open(filepath.Clean(cgroupFile))
-	if err != nil {
-		logger.Error(nil, "could not open cgroup", "process-id", processID)
-		return ""
-	}
-	defer func() {
-		cerr := file.Close()
-		if err == nil {
-			err = cerr
 		}
-	}()
-
-	// extracts crio format from cgroup:
-	// 0::/system.slice/crio-conmon-5819a498721cf8bb7e334809c9e48aa310bfc98801eb8017034ad17fb0749920.scope
-	podIDRegex := regexp.MustCompile(`^0.+-([a-f0-9]+)\.scope$`)
-	scanner := bufio.NewScanner(file)
-	scanner.Split(bufio.ScanLines)
-
-	for scanner.Scan() {
-		capture := podIDRegex.FindStringSubmatch(scanner.Text())
-		if len(capture) > 0 {
-			return capture[1]
+		offset += int64(readBytes)
+		if readBytes != 0 {
+			lines <- string(buffer[:readBytes])
 		}
+		time.Sleep(time.Second)
 	}
-	return ""
 }
diff --git a/internal/pkg/manager/spod/bindata/spod.go b/internal/pkg/manager/spod/bindata/spod.go
index 47af2dbd0..a3eccef3b 100644
--- a/internal/pkg/manager/spod/bindata/spod.go
+++ b/internal/pkg/manager/spod/bindata/spod.go
@@ -30,7 +30,7 @@ var (
 	truly                           = true
 	userRoot                  int64 = 0
 	userRootless                    = int64(config.UserRootless)
-	hostCharDev                     = v1.HostPathCharDev
+	hostPathFile                    = v1.HostPathFile
 	hostPathDirectory               = v1.HostPathDirectory
 	hostPathDirectoryOrCreate       = v1.HostPathDirectoryOrCreate
 )
@@ -40,7 +40,7 @@ const (
 	SelinuxdPrivateDir   = "/var/run/selinuxd"
 	SelinuxdSocketPath   = SelinuxdPrivateDir + "/selinuxd.sock"
 	SelinuxdDBPath       = SelinuxdPrivateDir + "/selinuxd.db"
-	kmsgPath             = "/dev/kmsg"
+	varLogSpoPath        = "/var/log/spo.log"
 )
 
 var Manifest = &appsv1.DaemonSet{
@@ -288,16 +288,13 @@ var Manifest = &appsv1.DaemonSet{
 						ImagePullPolicy: v1.PullAlways,
 						VolumeMounts: []v1.VolumeMount{
 							{
-								Name:      "host-devkmsg-volume",
-								MountPath: kmsgPath,
+								Name:      "host-varlogspo-volume",
+								MountPath: varLogSpoPath,
 								ReadOnly:  true,
 							},
 						},
 						SecurityContext: &v1.SecurityContext{
-							// /dev/kmsg is being used as kernel messages source in order to
-							// support the widest range of distros. However, access to
-							// devices requires the container to run as privileged.
-							Privileged:             &truly,
+							Privileged:             &falsely,
 							ReadOnlyRootFilesystem: &truly,
 							SELinuxOptions: &v1.SELinuxOptions{
 								// TODO(pjbgf): Use a more restricted selinux type
@@ -416,11 +413,11 @@ var Manifest = &appsv1.DaemonSet{
 						},
 					},
 					{
-						Name: "host-devkmsg-volume",
+						Name: "host-varlogspo-volume",
 						VolumeSource: v1.VolumeSource{
 							HostPath: &v1.HostPathVolumeSource{
-								Path: kmsgPath,
-								Type: &hostCharDev,
+								Path: varLogSpoPath,
+								Type: &hostPathFile,
 							},
 						},
 					},
diff --git a/internal/pkg/webhooks/binding/binding.go b/internal/pkg/webhooks/binding/binding.go
index 6ef3fcf09..a59f1b4d1 100644
--- a/internal/pkg/webhooks/binding/binding.go
+++ b/internal/pkg/webhooks/binding/binding.go
@@ -80,7 +80,9 @@ func newContainerMap(spec *corev1.PodSpec) containerMap {
 
 // Leader election
 // nolint:lll
-// +kubebuilder:rbac:groups=core,namespace="security-profiles-operator",resources=configmaps;events,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=core,namespace="security-profiles-operator",resources=configmaps,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=coordination.k8s.io,namespace="security-profiles-operator",resources=leases,verbs=create;get;update;
 
 func (p *podSeccompBinder) Handle(ctx context.Context, req admission.Request) admission.Response { //nolint:gocritic
 	profileBindings := &profilebindingv1alpha1.ProfileBindingList{}
diff --git a/internal/pkg/webhooks/recording/recording.go b/internal/pkg/webhooks/recording/recording.go
index df68f25b4..7f64545ab 100644
--- a/internal/pkg/webhooks/recording/recording.go
+++ b/internal/pkg/webhooks/recording/recording.go
@@ -64,7 +64,9 @@ func RegisterWebhook(server *webhook.Server, c client.Client) {
 
 // nolint:lll
 // Leader election
-// +kubebuilder:rbac:groups=core,namespace="security-profiles-operator",resources=configmaps;events,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=core,namespace="security-profiles-operator",resources=configmaps,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch
+// +kubebuilder:rbac:groups=coordination.k8s.io,namespace="security-profiles-operator",resources=leases,verbs=create;get;update;
 
 // nolint:gocritic
 func (p *podSeccompRecorder) Handle(
diff --git a/vendor/github.com/cirocosta/dmesg_exporter/LICENSE b/vendor/github.com/cirocosta/dmesg_exporter/LICENSE
deleted file mode 100644
index 701501012..000000000
--- a/vendor/github.com/cirocosta/dmesg_exporter/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2019 Ciro S. Costa
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/vendor/github.com/cirocosta/dmesg_exporter/kmsg/kmsg.go b/vendor/github.com/cirocosta/dmesg_exporter/kmsg/kmsg.go
deleted file mode 100644
index f6e62a990..000000000
--- a/vendor/github.com/cirocosta/dmesg_exporter/kmsg/kmsg.go
+++ /dev/null
@@ -1,234 +0,0 @@
-// Package kmsg provides a minimal interface for dealing with
-// kmsg messages extracted from `/dev/kmesg`.
-package kmsg
-
-import (
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/pkg/errors"
-)
-
-type Priority uint8
-
-const (
-	PriorityEmerg Priority = iota
-	PriorityAlert
-	PriorityCrit
-	PriorityErr
-	PriorityWarning
-	PriorityNotice
-	PriorityInfo
-	PriorityDebug
-)
-
-func (p Priority) String() (s string) {
-	switch p {
-	case PriorityEmerg:
-		s = "emerg"
-	case PriorityAlert:
-		s = "alert"
-	case PriorityCrit:
-		s = "crit"
-	case PriorityErr:
-		s = "err"
-	case PriorityWarning:
-		s = "warning"
-	case PriorityNotice:
-		s = "notice"
-	case PriorityInfo:
-		s = "info"
-	case PriorityDebug:
-		s = "debug"
-	default:
-		s = "unknown"
-
-	}
-
-	return
-}
-
-type Facility uint8
-
-const (
-	FacilityKern Facility = iota
-	FacilityUser
-	FacilityMail
-	FacilityDaemon
-	FacilityAuth
-	FacilitySyslog
-	FacilityLpr
-	FacilityNews
-
-	FacilityUnknown // custom facility used to delimite those that we know
-)
-
-func (f Facility) String() (s string) {
-	switch f {
-	case FacilityKern:
-		s = "kern"
-	case FacilityUser:
-		s = "user"
-	case FacilityMail:
-		s = "mail"
-	case FacilityDaemon:
-		s = "daemon"
-	case FacilityAuth:
-		s = "auth"
-	case FacilitySyslog:
-		s = "syslog"
-	case FacilityLpr:
-		s = "lpr"
-	case FacilityNews:
-		s = "news"
-	default:
-		s = "unknown"
-	}
-
-	return
-}
-
-type Flag byte
-
-const (
-	FlagUnknown Flag = iota
-
-	FlagDefault           // -: default flag
-	FlagFragment          // c: indicates the fragment of a line
-	FlagFragmentFollowing // +: continuation of a fragment
-
-)
-
-// DecodeFlag decodes the raw byte flag into a type Flag object.
-func DecodeFlag(rawFlag byte) (flag Flag) {
-	switch rawFlag {
-	case '-':
-		flag = FlagDefault
-	case 'c':
-		flag = FlagFragment
-	case '+':
-		flag = FlagFragmentFollowing
-	default:
-		flag = FlagUnknown
-	}
-
-	return
-}
-
-func IsValidFacility(facility uint8) (isValid bool) {
-	isValid = (facility < uint8(FacilityUnknown))
-	return
-}
-
-type Message struct {
-	Facility       Facility
-	Flag           Flag
-	Message        string
-	Priority       Priority
-	SequenceNumber int64
-	Timestamp      time.Time
-}
-
-// DecodePrefix extracts both priority and facility from a given
-// syslog(2) encoded prefix.
-//
-//	   facility    priority
-//      .-----------.  .-----.
-//      |           |  |     |
-//	7  6  5  4  3  2  1  0    bits
-//
-// ps.: the priority does not need to be verified because we're
-//      picking the first 3 bits and there's no way of having a
-//	wrong priority given that the set of possible values has
-//	8 numbers.
-func DecodePrefix(prefix uint8) (priority Priority, facility Facility) {
-	const priortyMask uint8 = (1 << 3) - 1
-
-	facilityNum := prefix >> 3
-
-	if !IsValidFacility(facilityNum) {
-		facility = FacilityUnknown
-	} else {
-		facility = Facility(facilityNum)
-	}
-
-	priority = Priority(prefix & priortyMask)
-
-	return
-}
-
-// Parse takes care of parsing a `kmsg` message acording to the kernel
-// documentation at https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg.
-//
-// REGULAR MESSAGE:
-//
-//                  INFO		              MSG
-//     .------------------------------------------. .------.
-//    |                                            |        |
-//    |	int	int      int      char, <ignore>   | string |
-//    prefix  , seq, timestamp_us,flag[,..........];<message>
-//
-//
-// CONTINUATION:
-//
-//	    | key | value |
-//	/x7F<THIS>=<THATTT>
-//
-func Parse(rawMsg string) (m *Message, err error) {
-	if rawMsg == "" {
-		err = errors.Errorf("msg must not be empty")
-		return
-	}
-
-	splittedMessage := strings.SplitN(rawMsg, ";", 2)
-	if len(splittedMessage) < 2 {
-		err = errors.Errorf("message field not present")
-		return
-	}
-
-	m = new(Message)
-
-	infoSection := splittedMessage[0]
-	m.Message = strings.TrimSpace(splittedMessage[1])
-
-	splittedInfoSection := strings.SplitN(infoSection, ",", 5)
-	if len(splittedInfoSection) < 4 {
-		err = errors.Errorf("info section with not enought fields")
-		return
-	}
-
-	prefix, err := strconv.ParseInt(splittedInfoSection[0], 10, 8)
-	if err != nil {
-		err = errors.Wrapf(err,
-			"couldn't convert priority to int")
-		return
-	}
-
-	m.Priority, m.Facility = DecodePrefix(uint8(prefix))
-
-	m.SequenceNumber, err = strconv.ParseInt(splittedInfoSection[1], 10, 64)
-	if err != nil {
-		err = errors.Wrapf(err,
-			"couldn't convert sequence number to int64")
-		return
-	}
-
-	timestamp, err := strconv.ParseInt(splittedInfoSection[2], 10, 64)
-	if err != nil {
-		err = errors.Wrapf(err,
-			"couldn't convert sequence number to int64")
-		return
-	}
-
-	m.Timestamp = time.Unix(timestamp/int64(time.Millisecond), 0)
-
-	if len(splittedInfoSection[3]) != 1 {
-		err = errors.Errorf("flag must be a single char")
-		return
-	}
-
-	m.Flag = DecodeFlag(splittedInfoSection[3][0])
-
-	return
-}
diff --git a/vendor/github.com/cirocosta/dmesg_exporter/reader/reader.go b/vendor/github.com/cirocosta/dmesg_exporter/reader/reader.go
deleted file mode 100644
index accb40041..000000000
--- a/vendor/github.com/cirocosta/dmesg_exporter/reader/reader.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package reader
-
-import (
-	"context"
-	"io"
-	"syscall"
-
-	"github.com/cirocosta/dmesg_exporter/kmsg"
-	"github.com/pkg/errors"
-)
-
-type Reader interface {
-	Listen(ctx context.Context, messages chan<- *kmsg.Message) (errors chan error)
-}
-
-type kmsgReader struct {
-	file io.Reader
-}
-
-func NewReader(r io.Reader) (reader Reader) {
-	reader = &kmsgReader{
-		file: r,
-	}
-
-	return
-}
-
-const readBufferSize = 4096
-
-func (r *kmsgReader) Listen(ctx context.Context, messages chan<- *kmsg.Message) (errChan chan error) {
-	errChan = make(chan error, 1)
-
-	go func() {
-		defer close(errChan)
-
-		err := r.consumeDevice(ctx, messages)
-		if err != nil {
-			errChan <- err
-			return
-		}
-
-		return
-	}()
-
-	return
-}
-
-func (r *kmsgReader) consumeDevice(ctx context.Context, messages chan<- *kmsg.Message) (err error) {
-	var (
-		buffer  = make([]byte, readBufferSize)
-		n       int
-		message *kmsg.Message
-		done    = make(chan error, 1)
-	)
-
-	defer close(messages)
-
-	go func() {
-		var err error
-
-		defer func() {
-			done <- err
-		}()
-
-		for {
-			n, err = r.file.Read(buffer)
-			if err != nil {
-				if err == io.EOF {
-					err = nil
-					return
-				}
-
-				if err == syscall.EPIPE {
-					err = nil
-					return
-				}
-
-				err = errors.Wrapf(err,
-					"unexpected failure while reading file")
-				return
-			}
-
-			message, err = kmsg.Parse(string(buffer[:n]))
-			if err != nil {
-				err = errors.Wrapf(err,
-					"failed to parse raw message")
-				return
-			}
-
-			messages <- message
-		}
-	}()
-
-	select {
-	case err = <-done:
-	case <-ctx.Done():
-		err = ctx.Err()
-	}
-
-	return
-}
diff --git a/vendor/k8s.io/klog/v2/klog.go b/vendor/k8s.io/klog/v2/klog.go
index fe2b724f6..e850db6b0 100644
--- a/vendor/k8s.io/klog/v2/klog.go
+++ b/vendor/k8s.io/klog/v2/klog.go
@@ -433,7 +433,7 @@ func InitFlags(flagset *flag.FlagSet) {
 	flagset.Var(&logging.verbosity, "v", "number for the log level verbosity")
 	flagset.BoolVar(&logging.addDirHeader, "add_dir_header", logging.addDirHeader, "If true, adds the file directory to the header of the log messages")
 	flagset.BoolVar(&logging.skipHeaders, "skip_headers", logging.skipHeaders, "If true, avoid header prefixes in the log messages")
-	flagset.BoolVar(&logging.oneOutput, "one_output", logging.oneOutput, "If true, only write logs to their native severity level (vs also writing to each lower severity level")
+	flagset.BoolVar(&logging.oneOutput, "one_output", logging.oneOutput, "If true, only write logs to their native severity level (vs also writing to each lower severity level)")
 	flagset.BoolVar(&logging.skipLogHeaders, "skip_log_headers", logging.skipLogHeaders, "If true, avoid headers when opening log files")
 	flagset.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr")
 	flagset.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging")
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 95c9de2c7..b6835a630 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -4,10 +4,6 @@ github.com/beorn7/perks/quantile
 github.com/blang/semver
 # github.com/cespare/xxhash/v2 v2.1.1
 github.com/cespare/xxhash/v2
-# github.com/cirocosta/dmesg_exporter v0.0.0-20190515130104-6e3e42fc8d7b
-## explicit
-github.com/cirocosta/dmesg_exporter/kmsg
-github.com/cirocosta/dmesg_exporter/reader
 # github.com/containers/common v0.35.2
 ## explicit
 github.com/containers/common/pkg/seccomp
@@ -404,7 +400,7 @@ k8s.io/client-go/util/workqueue
 # k8s.io/component-base v0.20.2
 k8s.io/component-base/config
 k8s.io/component-base/config/v1alpha1
-# k8s.io/klog/v2 v2.5.0
+# k8s.io/klog/v2 v2.6.0
 ## explicit
 k8s.io/klog/v2
 k8s.io/klog/v2/klogr


Test patch:
diff --git a/internal/pkg/daemon/enricher/audit_test.go b/internal/pkg/daemon/enricher/audit_test.go
index c7e126742..0eaebb275 100644
--- a/internal/pkg/daemon/enricher/audit_test.go
+++ b/internal/pkg/daemon/enricher/audit_test.go
@@ -30,11 +30,17 @@ func Test_isAuditLine(t *testing.T) {
 		want    bool
 	}{
 		{
-			"Should identify seccomp log lines",
+			"Should identify type=1326 log lines",
 			//nolint:lll
 			`audit: type=1326 audit(1611996299.149:466250): auid=4294967295 uid=0 gid=0 ses=4294967295 pid=615549 comm="sh" exe="/bin/busybox" sig=0 arch=c000003e syscall=1 compat=0 ip=0x7f61a81c5923 code=0x7ffc0000`,
 			true,
 		},
+		{
+			"Should identify type=SECCOMP log lines",
+			//nolint:lll
+			`type=SECCOMP msg=audit(1613596317.899:6461): auid=4294967295 uid=0 gid=0 ses=4294967295 subj=system_u:system_r:spc_t:s0:c284,c594 pid=2039886 comm="ls" exe="/bin/ls" sig=0 arch=c000003e syscall=3 compat=0 ip=0x7f62dce3d4c7 code=0x7ffc0000AUID="unset" UID="root" GID="root" ARCH=x86_64 SYSCALL=close`,
+			true,
+		},
 		{
 			"Should ignore unsupported log types",
 			//nolint:lll
@@ -46,6 +52,12 @@ func Test_isAuditLine(t *testing.T) {
 			`type=1326 syscall=1`,
 			false,
 		},
+		{
+			"Should identify SELinux log lines",
+			//nolint:lll
+			`type=AVC msg=audit(1613173578.156:2945): avc:  denied  { read } for  pid=75593 comm="security-profil" name="token" dev="tmpfs" ino=612459 scontext=system_u:system_r:container_t:s0:c4,c808 tcontext=system_u:object_r:var_lib_t:s0 tclass=lnk_file permissive=0`,
+			true,
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
@@ -78,6 +90,32 @@ func Test_extractAuditLine(t *testing.T) {
 			},
 			nil,
 		},
+		{
+			"Should extract seccomp log lines",
+			//nolint:lll
+			`type=SECCOMP msg=audit(1613596317.899:6461): auid=4294967295 uid=0 gid=0 ses=4294967295 subj=system_u:system_r:spc_t:s0:c284,c594 pid=2039886 comm="ls" exe="/bin/ls" sig=0 arch=c000003e syscall=3 compat=0 ip=0x7f62dce3d4c7 code=0x7ffc0000AUID="unset" UID="root" GID="root" ARCH=x86_64 SYSCALL=close`,
+			&auditLine{
+				Type:         "seccomp",
+				TimestampID:  "1613596317.899:6461",
+				SystemCallID: 3,
+				ProcessID:    2039886,
+				Executable:   "/bin/ls",
+			},
+			nil,
+		},
+		{
+			"Should extract selinux log lines",
+			//nolint:lll
+			`type=AVC msg=audit(1613173578.156:2945): avc:  denied  { read } for  pid=75593 comm="security-profil" name="token" dev="tmpfs" ino=612459 scontext=system_u:system_r:container_t:s0:c4,c808 tcontext=system_u:object_r:var_lib_t:s0 tclass=lnk_file permissive=0`,
+			&auditLine{
+				Type:         "selinux",
+				TimestampID:  "1613173578.156:2945",
+				SystemCallID: 0,
+				ProcessID:    75593,
+				Executable:   "",
+			},
+			nil,
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
diff --git a/internal/pkg/daemon/enricher/container_test.go b/internal/pkg/daemon/enricher/container_test.go
new file mode 100644
index 000000000..ead15c507
--- /dev/null
+++ b/internal/pkg/daemon/enricher/container_test.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package enricher
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestExtractContainerID(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		name       string
+		cgroupLine string
+		want       string
+	}{
+		{
+			"Should extract crio ID",
+			//nolint:lll
+			`4:net_cls,net_prio:/kubepods/besteffort/pod26ba375c-2266-4ecc-bf2d-b626db8762af/crio-af208fd68bf39a07a439ed0c9b6609b9ae63ecd8a5f1a2af3e0db48b945b320a`,
+			"af208fd68bf39a07a439ed0c9b6609b9ae63ecd8a5f1a2af3e0db48b945b320a",
+		},
+		{
+			"Should return empty when not found",
+			`0::/system.slice/crio.service`,
+			"",
+		},
+		{
+			"Should extract crio ID ending with .scope",
+			`0::/system.slice/crio-conmon-5819a498721cf8bb7e334809c9e48aa310bfc98801eb8017034ad17fb0749920.scope`,
+			"5819a498721cf8bb7e334809c9e48aa310bfc98801eb8017034ad17fb0749920",
+		},
+	}
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			got := extractID(tt.cgroupLine)
+			require.Equal(t, tt.want, got)
+		})
+	}
+}