Skip to content

Commit 14f98f8

Browse files
authored
Filter ContainerEnumeration output (google#1351)
* Filter ContainerEnumeration output * Fix syntax/formatting * Fix var name * Update log messages * Add more filters, update messages * Fix image filter * Update container evidence names and enumeration report * update summary message * yapf * Set UnknownImageName if no image name exists * Add default container/namespace values
1 parent 405b657 commit 14f98f8

File tree

4 files changed

+199
-14
lines changed

4 files changed

+199
-14
lines changed

k8s/tools/get-system-pods-list.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2023 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Script to enumerate system pods.
15+
16+
This is to generate a list of containers that we can filter out by default in
17+
the containr/docker enumeration tasks.
18+
"""
19+
20+
import json
21+
import subprocess
22+
import sys
23+
24+
if len(sys.argv) < 3:
25+
print(
26+
f'usage: {sys.argv[0]} <project> <cluster to list> [<zone if other than us-central-f>]'
27+
)
28+
sys.exit(1)
29+
30+
project = sys.argv[1]
31+
cluster = sys.argv[2]
32+
if len(sys.argv) >= 4:
33+
zone = sys.argv[3]
34+
else:
35+
zone = 'us-central1-f'
36+
namespaces = {}
37+
38+
# Authenticate to cluster
39+
auth_cmd = f'gcloud container clusters get-credentials {cluster} --zone {zone} --project {project}'
40+
print(f'Authenticating to project {project} cluster {cluster} zone {zone}')
41+
subprocess.check_call(auth_cmd.split(' '))
42+
43+
# Get pods data
44+
cmd = f"kubectl get pods -o json -A"
45+
pods_data = subprocess.check_output(cmd.split(' '))
46+
pods_data = json.loads(pods_data)
47+
48+
filtered_data = []
49+
50+
for item in pods_data['items']:
51+
if not item.get('metadata'):
52+
continue
53+
54+
name = item.get('metadata').get('name')
55+
namespace = item.get('metadata').get('namespace')
56+
57+
if namespace not in namespaces:
58+
namespaces[namespace] = []
59+
60+
for container in item['spec']['containers']:
61+
image = container["image"].split('@')[0]
62+
image = image.split(':')[0]
63+
namespaces[namespace].append(
64+
f'Pod Name: {name}, Container Name: {container["name"]} Image: {image}')
65+
66+
print()
67+
for namespace, containers in namespaces.items():
68+
print(f'Namespace: {namespace}')
69+
for container_info in containers:
70+
print(f'\t{container_info}')

turbinia/evidence.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -1274,11 +1274,15 @@ class ContainerdContainer(Evidence):
12741274
REQUIRED_ATTRIBUTES = ['namespace', 'container_id']
12751275
POSSIBLE_STATES = [EvidenceState.CONTAINER_MOUNTED]
12761276

1277-
def __init__(self, namespace=None, container_id=None, *args, **kwargs):
1277+
def __init__(
1278+
self, namespace=None, container_id=None, image_name=None, pod_name=None,
1279+
*args, **kwargs):
12781280
"""Initialization of containerd container."""
12791281
super(ContainerdContainer, self).__init__(*args, **kwargs)
12801282
self.namespace = namespace
12811283
self.container_id = container_id
1284+
self.image_name = image_name if image_name else 'UnknownImageName'
1285+
self.pod_name = pod_name if pod_name else 'UnknownPodName'
12821286
self._image_path = None
12831287
self._container_fs_path = None
12841288

@@ -1290,9 +1294,12 @@ def name(self):
12901294
return self._name
12911295

12921296
if self.parent_evidence:
1293-
return ':'.join((self.parent_evidence.name, self.container_id))
1297+
return ':'.join((
1298+
self.parent_evidence.name, self.image_name, self.pod_name,
1299+
self.container_id))
12941300
else:
1295-
return ':'.join((self.type, self.container_id))
1301+
return ':'.join(
1302+
(self.type, self.image_name, self.pod_name, self.container_id))
12961303

12971304
def _preprocess(self, _, required_states):
12981305
if EvidenceState.CONTAINER_MOUNTED in required_states:

turbinia/workers/containerd.py

+117-11
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,54 @@
3232

3333
CE_BINARY = '/opt/container-explorer/bin/ce'
3434
CE_SUPPORT_FILE = '/opt/container-explorer/etc/supportcontainer.yaml'
35+
POD_NAME_LABEL = 'io.kubernetes.pod.name'
3536

3637

3738
class ContainerdEnumerationTask(TurbiniaTask):
3839
"""Enumerate containerd containers on Linux."""
3940

4041
REQUIRED_STATES = [state.ATTACHED, state.MOUNTED]
4142

43+
TASK_CONFIG = {
44+
# These filters will all match on partial matches, e.g. an image filter of
45+
# ['gke.gcr.io/'] will filter out image `gke.gcr.io/event-exporter`.
46+
#
47+
# Which k8 namespaces to filter out by default
48+
'filter_namespaces': ['kube-system'],
49+
'filter_pod_names': ['sidecar', 'k8s-sidecar', 'konnectivity-agent'],
50+
# Taken from
51+
# https://github.com/google/container-explorer/blob/main/supportcontainer.yaml
52+
'filter_images': [
53+
'gcr.io/gke-release-staging/cluster-proportional-autoscaler-amd64',
54+
'gcr.io/k8s-ingress-image-push/ingress-gce-404-server-with-metrics',
55+
'gke.gcr.io/ingress-gce-404-server-with-metrics',
56+
'gke.gcr.io/cluster-proportional-autoscaler',
57+
'gke.gcr.io/csi-node-driver-registrar',
58+
'gke.gcr.io/event-exporter',
59+
'gke.gcr.io/fluent-bit',
60+
'gke.gcr.io/fluent-bit-gke-exporter',
61+
'gke.gcr.io/gcp-compute-persistent-disk-csi-driver',
62+
'gke.gcr.io/gke-metrics-agent',
63+
'gke.gcr.io/k8s-dns-dnsmasq-nanny',
64+
'gke.gcr.io/k8s-dns-kube-dns',
65+
'gke.gcr.io/k8s-dns-sidecar',
66+
'gke.gcr.io/kube-proxy-amd64',
67+
'gke.gcr.io/prometheus-to-sd',
68+
'gke.gcr.io/proxy-agent',
69+
'k8s.gcr.io/metrics-server/metrics-server',
70+
'gke.gcr.io/metrics-server',
71+
'k8s.gcr.io/pause',
72+
'gke.gcr.io/pause',
73+
'gcr.io/gke-release-staging/addon-resizer',
74+
'gcr.io/gke-release-staging/cpvpa-amd64',
75+
'gcr.io/google-containers/pause-amd64',
76+
'gke.gcr.io/addon-resizer',
77+
'gke.gcr.io/cpvpa-amd64',
78+
'k8s.gcr.io/kube-proxy-amd64',
79+
'k8s.gcr.io/prometheus-to-sd',
80+
],
81+
}
82+
4283
def list_containers(self, evidence, _, detailed_output=False):
4384
"""List containerd containers in the evidence.
4485
@@ -95,8 +136,8 @@ def _list_containers_result(self, containers, detailed_output):
95136
return containers
96137

97138
basic_fields = [
98-
'Namespace', 'Image', 'ContainerType', 'ID', 'Hostname', 'CreatedAt'
99-
'Labels'
139+
'Name', 'Namespace', 'Image', 'ContainerType', 'ID', 'Hostname',
140+
'CreatedAt', 'Labels'
100141
]
101142
basic_containers = []
102143

@@ -123,10 +164,16 @@ def run(self, evidence, result):
123164
summary = ''
124165
success = False
125166
report_data = []
167+
filter_namespaces = self.task_config.get('filter_namespaces')
168+
filter_pod_names = self.task_config.get('filter_pod_names')
169+
filter_images = self.task_config.get('filter_images')
170+
filtered_container_list = []
126171

127172
image_path = evidence.mount_path
128173
if not image_path:
129-
summary = f'Evidence {evidence.name}:{evidence.source_path} is not mounted'
174+
summary = (
175+
f'Evidence {evidence.name}:{evidence.source_path} is not '
176+
'mounted')
130177
result.close(self, success=False, status=summary)
131178
return result
132179

@@ -142,20 +189,62 @@ def run(self, evidence, result):
142189
f'Found {len(container_ids)} containers: {", ".join(container_ids)}')
143190

144191
# 2. Add containers as evidences
192+
new_evidence = []
145193
for container in containers:
146-
namespace = container.get('Namespace')
147-
container_id = container.get('ID')
194+
namespace = container.get('Namespace', 'UnknownNamespace')
195+
container_id = container.get('ID', 'UnknownContainerID')
196+
if container.get('Labels'):
197+
pod_name = container.get('Labels').get(
198+
POD_NAME_LABEL, 'UnknownPodName')
199+
else:
200+
pod_name = 'UnknownPodName'
148201
container_type = container.get('ContainerType') or None
202+
image = container.get('Image')
203+
if image:
204+
image_short = image.split('@')[0]
205+
image_short = image_short.split(':')[0]
206+
else:
207+
image_short = 'UnknownImageName'
149208

150209
if not namespace or not container_id:
151-
result.log(
152-
f'Value is empty. namespace={namespace}, container_id={container_id}'
153-
)
154-
report_data.append(
210+
message = (
155211
f'Skipping container with empty value namespace ({namespace})'
156212
f' or container_id ({container_id})')
213+
result.log(message)
214+
report_data.append(message)
157215
continue
158216

217+
# Filter out configured namespaces/containers/images. Even though we
218+
# could let container explorer filter these before we get them we want
219+
# to do it here so that we can report on what was available and filtered
220+
# out to give the analyst the option to reprocess these containers.
221+
if filter_namespaces:
222+
if namespace in filter_namespaces:
223+
message = (
224+
f'Filtering out container {container_id} because namespace '
225+
f'matches filter.')
226+
result.log(message)
227+
report_data.append(message)
228+
filtered_container_list.append(container_id)
229+
continue
230+
if filter_images:
231+
if image_short in filter_images:
232+
message = (
233+
f'Filtering out image {image} because image matches filter')
234+
result.log(message)
235+
report_data.append(message)
236+
filtered_container_list.append(container_id)
237+
continue
238+
if filter_pod_names:
239+
if pod_name in filter_pod_names:
240+
message = (
241+
f'Filtering out container {container_id} because container '
242+
f'name matches filter')
243+
result.log(message)
244+
report_data.append(message)
245+
filtered_container_list.append(container_id)
246+
continue
247+
159248
# We want to process docker managed container using Docker-Explorer
160249
if container_type and container_type.lower() == 'docker':
161250
result.log(
@@ -165,12 +254,29 @@ def run(self, evidence, result):
165254
continue
166255

167256
container_evidence = ContainerdContainer(
168-
namespace=namespace, container_id=container_id)
257+
namespace=namespace, container_id=container_id,
258+
image_name=image_short, pod_name=pod_name)
259+
new_evidence.append(container_evidence.name)
169260

170261
result.add_evidence(container_evidence, evidence.config)
262+
result.log(
263+
f'Adding container evidence {container_evidence.name} '
264+
f'type {container_type}')
265+
171266
summary = (
172-
f'Found {len(container_ids)} containers: {", ".join(container_ids)}')
267+
f'Found {len(container_ids)} containers, added {len(new_evidence)} '
268+
f'(filtered out {len(filtered_container_list)})')
173269
success = True
270+
if filtered_container_list:
271+
report_data.append(
272+
f'Filtered out {len(filtered_container_list)} containers: '
273+
f'{", ".join(filtered_container_list)}')
274+
report_data.append(
275+
f'Container filter lists: Namespaces: {filter_namespaces}, Images: {filter_images}, '
276+
f'Pod Names: {filter_pod_names}')
277+
report_data.append(
278+
'To process filtered containers, adjust the ContainerEnumeration '
279+
'Task config filter* parameters with a recipe')
174280
except TurbiniaException as e:
175281
summary = f'Error enumerating containerd containers: {e}'
176282
report_data.append(summary)

turbinia/workers/containerd_test.py

+2
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ def testContainerdEnumerationTaskRun(self, list_containers_mock, _):
4747
{
4848
'Namespace': 'default',
4949
'ID': 'nginx01',
50+
'Image': 'nginx01-image',
5051
},
5152
{
5253
'Namespace': 'default',
5354
'ID': 'apache01',
55+
'Image': 'apache01-image',
5456
},
5557
]
5658
result = self.task.run(self.evidence, self.result)

0 commit comments

Comments
 (0)