Skip to content

Containers Data Source

The container enrichment feature gives Tracee the ability to extract details about active containers and link this information to the events it captures.

The data source feature makes the information gathered from active containers accessible to signatures. When an event is captured and triggers a signature, that signature can retrieve information about the container using its container ID, which is bundled with the event being analyzed by the signature.

Enabling the Feature

The data source does not need to be enabled, but requires that the container enrichment feature is. To enable the enrichment feature, execute trace with --containers. For more information you can read container enrichment page.

Internal Data Organization

From the data-sources documentation, you'll see that searches use keys. It's a bit like looking up information with a specific tag (or a key=value storage).

The containers data source operates straightforwardly. Using string keys, which represent the container IDs, you can fetch map[string]string values as shown below:

    schemaMap := map[string]string{
        "container_id":      "string",
        "container_name":    "string",
        "container_image":   "string",
        "k8s_pod_id":        "string",
        "k8s_pod_name":      "string",
        "k8s_pod_namespace": "string",
        "k8s_pod_sandbox":   "bool",
    }

From the structure above, using the container ID lets you access details like the originating Kubernetes pod name or the image utilized by the container.

Using the Containers Data Source

Make sure to read Golang Signatures first.

Signature Initialization

During the signature initialization, get the containers data source instance:

type e2eContainersDataSource struct {
    cb             detect.SignatureHandler
    containersData detect.DataSource
}

func (sig *e2eContainersDataSource) Init(ctx detect.SignatureContext) error {
    sig.cb = ctx.Callback
    containersData, ok := ctx.GetDataSource("tracee", "containers")
    if !ok {
        return fmt.Errorf("containers data source not registered")
    }
    sig.containersData = containersData
    return nil
}

Then, to each event being handled, you will Get(), from the data source, the information needed.

On Events

Given the following example:

func (sig *e2eContainersDataSource) OnEvent(event protocol.Event) error {
    eventObj, ok := event.Payload.(trace.Event)
    if !ok {
        return fmt.Errorf("failed to cast event's payload")
    }

    switch eventObj.EventName {
    case "sched_process_exec":
        containerId := eventObj.Container.ID
        if containerId == "" {
            return fmt.Errorf("received non container event")
        }

        container, err := sig.containersData.Get(containerId)
        if !ok {
            return fmt.Errorf("failed to find container in data source: %v", err)
        }

        containerImage, ok := container["container_image"].(string)
        if !ok {
            return fmt.Errorf("failed to obtain the container image name")
        }

        m, _ := sig.GetMetadata()

        sig.cb(detect.Finding{
            SigMetadata: m,
            Event:       event,
            Data:        map[string]interface{}{},
        })
    }

    return nil
}

You may see that, through the event object container ID information, you may query the data source and obtain the container name or any other information listed before.