diff --git a/grype-operator/Dockerfile b/grype-operator/Dockerfile new file mode 100644 index 00000000000..642537354fd --- /dev/null +++ b/grype-operator/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* +RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ + chmod +x kubectl && \ + mv kubectl /usr/local/bin/kubectl +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY operator.py . +CMD ["kopf", "run", "/app/operator.py", "--verbose", "--standalone", "--liveness=http://0.0.0.0:8080/healthz"] \ No newline at end of file diff --git a/grype-operator/README.md b/grype-operator/README.md new file mode 100644 index 00000000000..e6092728432 --- /dev/null +++ b/grype-operator/README.md @@ -0,0 +1,149 @@ +## Pre-Requisites + +- A Kubernetes Cluster (cloud managed or on-prem [eg. minikube,Kind,etc] ) +- kubectl cli install +- Basic understanding of kubernetes + +### Installation + +Although the namespace is already inside `grype-operator/manifests`. + +```bash +kubectl create ns grype-operator-system +``` + +Now we have to install crd,rbac,cm,etc in short all manifests from `grype-operator/manifests`. + +```bash +cd grype-operator +kubectl apply -f manifests/ +``` + +You should see operator pod running in ns : `grype-operator-system`, like below : + +```bash +kubectl get pods -n grype-operator-system + +NAME READY STATUS RESTARTS AGE +grype-operator-5645fcf69d-2mxf8 1/1 Running 0 46m +``` + +## Now Comes the interesting part, Testing the operator running 😘😎 + +```bash +kubectl run test-nginx --image=nginx:alpine +``` + +- Now as per our application or operator, grype operator will get triggered and create a job to scan the image used for above pod. +- When the job work is done , it will generate the vulnerability report for the scanned container/image and that can be checked with our custom resource and job pod is terminated. + +```bash +kubectl get vr or kubectl get vulnerabilityreport + +Output : + +NAME POD CRITICAL HIGH MEDIUM LOW AGE +test-nginx-74ccbdf4-report test-nginx 0 1 2 6 52m +``` + +### For In-Depth Vulnerability Report for the Image : + +```bash +kubectl describe vr test-nginx-74ccbdf4-report +``` + +Output : + +```bash +Name: test-nginx-74ccbdf4-report +Namespace: default +Labels: grype-operator.security.io/container=test-nginx + grype-operator.security.io/pod=test-nginx +Annotations: +API Version: security.grype.io/v1alpha1 +Kind: VulnerabilityReport +Metadata: + Creation Timestamp: 2025-11-10T11:37:52Z + Generation: 1 + Resource Version: 377766 + UID: 8d034f77-ae8d-4582-87be-6d92a7fe3978 +Spec: + Container Reports: + Container: test-nginx + Image: nginx:alpine + Summary: + Critical: 0 + High: 1 + Low: 6 + Medium: 2 + Negligible: 0 + Total: 9 + Unknown: 0 + Vulnerabilities: + Description: An out-of-memory flaw was found in libtiff. Passing a crafted tiff file to TIFFOpen() API may allow a remote attacker to cause a denial of service via a craft input with size smaller than 379 KB. + Id: CVE-2023-6277 + Package: tiff + Severity: Medium + Version: 4.7.1-r0 + Description: A segment fault (SEGV) flaw was found in libtiff that could be triggered by passing a crafted tiff file to the TIFFReadRGBATileExt() API. This flaw allows a remote attacker to cause a heap-buffer over + Id: CVE-2023-52356 + Package: tiff + Severity: High + Version: 4.7.1-r0 + Description: An issue was found in the tiffcp utility distributed by the libtiff package where a crafted TIFF file on processing may cause a heap-based buffer overflow leads to an application crash. + Id: CVE-2023-6228 + Package: tiff + Severity: Medium + Version: 4.7.1-r0 + Description: In tar in BusyBox through 1.37.0, a TAR archive can have filenames hidden from a listing through the use of terminal escape sequences. + Id: CVE-2025-46394 + Package: busybox + Severity: Low + Version: 1.37.0-r19 + Description: In tar in BusyBox through 1.37.0, a TAR archive can have filenames hidden from a listing through the use of terminal escape sequences. + Id: CVE-2025-46394 + Package: busybox-binsh + Severity: Low + Version: 1.37.0-r19 + Description: In tar in BusyBox through 1.37.0, a TAR archive can have filenames hidden from a listing through the use of terminal escape sequences. + Id: CVE-2025-46394 + Package: ssl_client + Severity: Low + Version: 1.37.0-r19 + Description: In netstat in BusyBox through 1.37.0, local users can launch of network application with an argv[0] containing an ANSI terminal escape sequence, leading to a denial of service (terminal locked up) whe + Id: CVE-2024-58251 + Package: busybox + Severity: Low + Version: 1.37.0-r19 + Description: In netstat in BusyBox through 1.37.0, local users can launch of network application with an argv[0] containing an ANSI terminal escape sequence, leading to a denial of service (terminal locked up) whe + Id: CVE-2024-58251 + Package: busybox-binsh + Severity: Low + Version: 1.37.0-r19 + Description: In netstat in BusyBox through 1.37.0, local users can launch of network application with an argv[0] containing an ANSI terminal escape sequence, leading to a denial of service (terminal locked up) whe + Id: CVE-2024-58251 + Package: ssl_client + Severity: Low + Version: 1.37.0-r19 + Pod: test-nginx + Scan Time: 2025-11-10T11:37:52.206036Z + Scanner: + Name: Grype + Vendor: Anchore + Summary: + Critical: 0 + High: 1 + Low: 6 + Medium: 2 + Negligible: 0 + Total: 9 + Unknown: 0 +Events: +``` + + +## Full Demo Video for Grype Operator for Kubernetes Clusters + + +https://github.com/user-attachments/assets/2dd85c3b-b30c-42c5-b589-efecd4423ab1 + diff --git a/grype-operator/manifests/configmap.yaml b/grype-operator/manifests/configmap.yaml new file mode 100644 index 00000000000..6583f62541e --- /dev/null +++ b/grype-operator/manifests/configmap.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grype-operator-config + namespace: grype-operator-system +data: + # Operator behavior + scan-on-create: "true" + scan-on-update: "true" + + # Scanner configuration + scanner-image: "anchore/grype:latest" + scanner-cpu-request: "100m" + scanner-memory-request: "256Mi" + scanner-cpu-limit: "500m" + scanner-memory-limit: "512Mi" + + # Job settings + job-ttl-seconds: "3600" + job-backoff-limit: "2" + + # Excluded namespaces (comma-separated) + excluded-namespaces: "kube-system,kube-public,kube-node-lease,grype-operator-system" \ No newline at end of file diff --git a/grype-operator/manifests/crd.yaml b/grype-operator/manifests/crd.yaml new file mode 100644 index 00000000000..e81764ac9e3 --- /dev/null +++ b/grype-operator/manifests/crd.yaml @@ -0,0 +1,136 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: vulnerabilityreports.security.grype.io +spec: + group: security.grype.io + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + pod: + type: string + description: Name of the scanned pod + scanTime: + type: string + description: Timestamp of the scan + scanner: + type: object + properties: + name: + type: string + vendor: + type: string + summary: + type: object + properties: + critical: + type: integer + high: + type: integer + medium: + type: integer + low: + type: integer + negligible: + type: integer + unknown: + type: integer + total: + type: integer + containerReports: + type: array + items: + type: object + properties: + container: + type: string + image: + type: string + error: + type: string + summary: + type: object + properties: + critical: + type: integer + high: + type: integer + medium: + type: integer + low: + type: integer + negligible: + type: integer + unknown: + type: integer + total: + type: integer + vulnerabilities: + type: array + items: + type: object + properties: + id: + type: string + severity: + type: string + package: + type: string + version: + type: string + fixedVersion: + type: array + items: + type: string + description: + type: string + urls: + type: array + items: + type: string + cvss: + type: array + items: + type: object + properties: + version: + type: string + vector: + type: string + score: + type: number + additionalPrinterColumns: + - name: Pod + type: string + jsonPath: .spec.pod + - name: Critical + type: integer + jsonPath: .spec.summary.critical + - name: High + type: integer + jsonPath: .spec.summary.high + - name: Medium + type: integer + jsonPath: .spec.summary.medium + - name: Low + type: integer + jsonPath: .spec.summary.low + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + scope: Namespaced + names: + plural: vulnerabilityreports + singular: vulnerabilityreport + kind: VulnerabilityReport + shortNames: + - vulnreport + - vr \ No newline at end of file diff --git a/grype-operator/manifests/deployment.yaml b/grype-operator/manifests/deployment.yaml new file mode 100644 index 00000000000..af65d2e5013 --- /dev/null +++ b/grype-operator/manifests/deployment.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grype-operator + namespace: grype-operator-system + labels: + app: grype-operator +spec: + replicas: 1 + selector: + matchLabels: + app: grype-operator + template: + metadata: + labels: + app: grype-operator + spec: + serviceAccountName: grype-operator + containers: + - name: operator + image: rohanrustagi18/grype-operator:latest + imagePullPolicy: IfNotPresent + env: + - name: OPERATOR_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + # securityContext: I commented these as they might cause issues in some environments (Beta Thing You know) + # allowPrivilegeEscalation: false + # runAsNonRoot: true + # runAsUser: 1000 + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} + # securityContext: I commented these as they might cause issues in some environments (Beta Thing You know) + # fsGroup: 1000 + # runAsNonRoot: true + # seccompProfile: + # type: RuntimeDefault diff --git a/grype-operator/manifests/namespace.yaml b/grype-operator/manifests/namespace.yaml new file mode 100644 index 00000000000..a0b67e8e22c --- /dev/null +++ b/grype-operator/manifests/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: grype-operator-system \ No newline at end of file diff --git a/grype-operator/manifests/rbac.yaml b/grype-operator/manifests/rbac.yaml new file mode 100644 index 00000000000..268493010be --- /dev/null +++ b/grype-operator/manifests/rbac.yaml @@ -0,0 +1,48 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: grype-operator-admin +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] +- nonResourceURLs: ["*"] + verbs: ["*"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: grype-scanner-default-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: grype-scanner-role +subjects: +- kind: ServiceAccount + name: grype-scanner + namespace: default + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: grype-scanner-admin +rules: +- apiGroups: ["*"] + resources: ["*"] + verbs: ["*"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: grype-scanner-admin-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: grype-scanner-admin +subjects: +- kind: ServiceAccount + name: grype-scanner + namespace: grype-operator-system \ No newline at end of file diff --git a/grype-operator/manifests/serviceaccount.yaml b/grype-operator/manifests/serviceaccount.yaml new file mode 100644 index 00000000000..128ae8b54c1 --- /dev/null +++ b/grype-operator/manifests/serviceaccount.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: grype-operator + namespace: grype-operator-system + +--- +# If you have other namespaces, create ServiceAccount there too +# Uncomment and modify as needed: +# +# apiVersion: v1 +# kind: ServiceAccount +# metadata: +# name: grype-scanner +# namespace: production +# --- +# apiVersion: v1 +# kind: ServiceAccount +# metadata: +# name: grype-scanner +# namespace: staging \ No newline at end of file diff --git a/grype-operator/operator.py b/grype-operator/operator.py new file mode 100644 index 00000000000..fa91d5971f3 --- /dev/null +++ b/grype-operator/operator.py @@ -0,0 +1,439 @@ +import kopf +import kubernetes +import json +import logging +import hashlib +from datetime import datetime +from typing import Dict, List, Any + +# Set more verbose logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +@kopf.on.startup() +def configure(settings: kopf.OperatorSettings, **_): + """Configure operator settings""" + settings.persistence.finalizer = 'grype-operator.security.io/finalizer' + settings.posting.level = logging.DEBUG + settings.watching.server_timeout = 60 + # Run cluster-wide to avoid the namespace warning + settings.watching.clusterwide = True + logger.info("Grype Operator starting up...") + +@kopf.on.create('pods') +@kopf.on.update('pods') +def handle_pod(spec, name, namespace, labels, uid, body, **kwargs): + """ + Create scan jobs for pod images when pods are created or updated + """ + logger.info(f"=== HANDLING POD: {namespace}/{name} ===") + logger.debug(f"Pod spec: {spec}") + logger.debug(f"Pod labels: {labels}") + logger.debug(f"Pod annotations: {kwargs.get('annotations', {})}") + + # Skip if already processed (check annotation) + annotations = kwargs.get('annotations', {}) + if annotations.get('grype-operator.security.io/scan-scheduled') == 'true': + logger.info(f"Pod {namespace}/{name} already has scan scheduled, skipping") + return + + # Skip system namespaces + excluded_namespaces = ['kube-system', 'kube-public', 'kube-node-lease', 'grype-operator-system'] + if namespace in excluded_namespaces: + logger.info(f"Skipping system namespace: {namespace}") + return + + # Skip scan job pods themselves (prevent infinite recursion) + if labels.get('app') == 'grype-scanner': + logger.info(f"Skipping scan job pod: {namespace}/{name}") + return + + # Skip pods created by jobs (scan jobs) + if labels.get('job-name') and 'grype-scan' in labels.get('job-name', ''): + logger.info(f"Skipping job pod: {namespace}/{name}") + return + + # Skip pods with grype-operator managed labels + if labels.get('grype-operator.security.io/managed') == 'true': + logger.info(f"Skipping operator-managed pod: {namespace}/{name}") + return + + containers = spec.get('containers', []) + init_containers = spec.get('initContainers', []) + + all_containers = containers + init_containers + logger.info(f"Found {len(all_containers)} containers in pod {namespace}/{name}") + + # Create scan jobs for each unique image + scanned_images = set() + for container in all_containers: + image = container.get('image') + container_name = container.get('name') + logger.info(f"Processing container: {container_name} with image: {image}") + + if image and image not in scanned_images: + logger.info(f"Creating scan job for image: {image} (container: {container_name})") + create_scan_job( + namespace=namespace, + pod_name=name, + pod_uid=uid, + container_name=container_name, + image=image + ) + scanned_images.add(image) + else: + logger.info(f"Skipping image {image} - already scanned or invalid") + + # Annotate pod as scan scheduled + if scanned_images: + logger.info(f"Annotating pod {namespace}/{name} with scan schedule") + api = kubernetes.client.CoreV1Api() + patch_body = { + "metadata": { + "annotations": { + "grype-operator.security.io/scan-scheduled": "true", + "grype-operator.security.io/schedule-time": datetime.utcnow().isoformat(), + "grype-operator.security.io/images-count": str(len(scanned_images)) + } + } + } + try: + api.patch_namespaced_pod(name, namespace, patch_body) + logger.info(f"Successfully annotated pod {namespace}/{name}") + except Exception as e: + logger.error(f"Failed to annotate pod {namespace}/{name}: {e}") + else: + logger.info(f"No images to scan in pod {namespace}/{name}") + +def create_scan_job(namespace: str, pod_name: str, pod_uid: str, + container_name: str, image: str): + """ + Create a Kubernetes Job to scan the image + """ + logger.info(f"Creating scan job in namespace {namespace} for image {image}") + + batch_api = kubernetes.client.BatchV1Api() + + # Generate unique job name + image_hash = hashlib.md5(image.encode()).hexdigest()[:8] + job_name = f"grype-scan-{pod_name}-{image_hash}"[:63] + report_name = f"{pod_name}-{image_hash}-report" + + # Escape image name for shell + escaped_image = image.replace('"', '\\"').replace('$', '\\$') + + # Create job manifest + job = { + 'apiVersion': 'batch/v1', + 'kind': 'Job', + 'metadata': { + 'name': job_name, + 'namespace': namespace, + 'labels': { + 'app': 'grype-scanner', + 'grype-operator.security.io/pod': pod_name, + 'grype-operator.security.io/managed': 'true', + 'grype-operator.security.io/job': 'true' + }, + 'ownerReferences': [{ + 'apiVersion': 'v1', + 'kind': 'Pod', + 'name': pod_name, + 'uid': pod_uid, + 'blockOwnerDeletion': False + }] + }, + 'spec': { + 'ttlSecondsAfterFinished': 3600, + 'backoffLimit': 3, + 'template': { + 'metadata': { + 'labels': { + 'app': 'grype-scanner', + 'grype-operator.security.io/pod': pod_name, + 'grype-operator.security.io/managed': 'true', + 'grype-operator.security.io/job': 'true' + } + }, + 'spec': { + 'restartPolicy': 'Never', + 'serviceAccountName': 'grype-scanner', + 'containers': [{ + 'name': 'grype-scanner', + 'image': 'alpine:latest', + 'command': ['/bin/sh'], + 'args': [ + '-c', + f'''#!/bin/sh +# Install kubectl first +echo "Installing kubectl..." +apk add --no-cache curl +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x ./kubectl +mv ./kubectl /usr/local/bin/kubectl + +# Install grype using official method +echo "Installing Grype..." +curl -sSfL https://get.anchore.io/grype | sh -s -- -b /usr/local/bin + +echo "=== Starting Grype Scan ===" +echo "Image: {escaped_image}" +echo "Pod: {pod_name}, Container: {container_name}" +echo "Namespace: {namespace}" + +# Update grype database +echo "Updating Grype database..." +grype db update || echo "Database update failed, but continuing..." + +# Run grype scan +echo "Running Grype scan..." +grype "{escaped_image}" -o json --quiet > /tmp/scan-results.json 2>/tmp/scan-errors.log || {{ + SCAN_EXIT_CODE=$? + echo "Grype scan failed with exit code: $SCAN_EXIT_CODE" + echo "Error output:" + cat /tmp/scan-errors.log + echo "Creating fallback report..." + cat > /tmp/scan-results.json << 'FALLBACK_EOF' +{{ + "matches": [], + "source": {{ + "target": "{escaped_image}", + "type": "image" + }}, + "distro": {{ + "name": "unknown", + "version": "unknown" + }}, + "descriptor": {{ + "name": "grype", + "version": "unknown" + }} +}} +FALLBACK_EOF +}} + +echo "Scan completed, processing results..." + +# Install Python for processing +apk add --no-cache python3 + +# Process results with Python +python3 << 'PYTHON_EOF' +import json +import sys +import os +from datetime import datetime +import subprocess + +try: + with open("/tmp/scan-results.json", "r") as f: + data = json.load(f) + print("Successfully loaded scan results") +except Exception as e: + print(f"Error loading scan results: {{e}}") + data = {{"matches": []}} + +matches = data.get("matches", []) +vulnerabilities = [] +summary = {{"critical": 0, "high": 0, "medium": 0, "low": 0, "negligible": 0, "unknown": 0, "total": 0}} + +for match in matches: + vuln = match.get("vulnerability", {{}}) + artifact = match.get("artifact", {{}}) + severity = vuln.get("severity", "Unknown").lower() + + vuln_data = {{ + "id": vuln.get("id", "UNKNOWN"), + "severity": vuln.get("severity", "Unknown"), + "package": artifact.get("name", "unknown"), + "version": artifact.get("version", "unknown"), + "fixedVersion": vuln.get("fix", {{}}).get("versions", []), + "description": vuln.get("description", "")[:200] + }} + vulnerabilities.append(vuln_data) + + if severity in summary: + summary[severity] += 1 + else: + summary["unknown"] += 1 + summary["total"] += 1 + +print(f"Found {{summary['total']}} vulnerabilities") + +# Create VulnerabilityReport +report_name = "{report_name}" +report = {{ + "apiVersion": "security.grype.io/v1alpha1", + "kind": "VulnerabilityReport", + "metadata": {{ + "name": report_name, + "namespace": "{namespace}", + "labels": {{ + "grype-operator.security.io/pod": "{pod_name}", + "grype-operator.security.io/container": "{container_name}" + }} + }}, + "spec": {{ + "pod": "{pod_name}", + "scanTime": datetime.utcnow().isoformat() + "Z", + "scanner": {{ + "name": "Grype", + "vendor": "Anchore" + }}, + "summary": summary, + "containerReports": [ + {{ + "container": "{container_name}", + "image": "{escaped_image}", + "summary": summary, + "vulnerabilities": vulnerabilities + }} + ] + }} +}} + +# Write report to file +with open("/tmp/report.yaml", "w") as f: + f.write("---\\n") + f.write("apiVersion: security.grype.io/v1alpha1\\n") + f.write("kind: VulnerabilityReport\\n") + f.write("metadata:\\n") + f.write(f" name: {report_name}\\n") + f.write(f" namespace: {namespace}\\n") + f.write(" labels:\\n") + f.write(f" grype-operator.security.io/pod: {pod_name}\\n") + f.write(f" grype-operator.security.io/container: {container_name}\\n") + f.write("spec:\\n") + f.write(f" pod: {pod_name}\\n") + f.write(f" scanTime: {{datetime.utcnow().isoformat()}}Z\\n") + f.write(" scanner:\\n") + f.write(" name: Grype\\n") + f.write(" vendor: Anchore\\n") + f.write(" summary:\\n") + f.write(f" critical: {{summary['critical']}}\\n") + f.write(f" high: {{summary['high']}}\\n") + f.write(f" medium: {{summary['medium']}}\\n") + f.write(f" low: {{summary['low']}}\\n") + f.write(f" negligible: {{summary['negligible']}}\\n") + f.write(f" unknown: {{summary['unknown']}}\\n") + f.write(f" total: {{summary['total']}}\\n") + f.write(" containerReports:\\n") + f.write(" - container: {container_name}\\n") + f.write(f" image: {escaped_image}\\n") + f.write(" summary:\\n") + f.write(f" critical: {{summary['critical']}}\\n") + f.write(f" high: {{summary['high']}}\\n") + f.write(f" medium: {{summary['medium']}}\\n") + f.write(f" low: {{summary['low']}}\\n") + f.write(f" negligible: {{summary['negligible']}}\\n") + f.write(f" unknown: {{summary['unknown']}}\\n") + f.write(f" total: {{summary['total']}}\\n") + f.write(" vulnerabilities:\\n") + for vuln in vulnerabilities: + f.write(" - id: " + vuln['id'] + "\\n") + f.write(" severity: " + vuln['severity'] + "\\n") + f.write(" package: " + vuln['package'] + "\\n") + f.write(" version: " + vuln['version'] + "\\n") + f.write(" description: " + vuln.get('description', '') + "\\n") + +print("VulnerabilityReport YAML generated successfully") + +# Apply using kubectl +try: + result = subprocess.run( + ["kubectl", "apply", "-f", "/tmp/report.yaml"], + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + print("VulnerabilityReport successfully applied!") + print(result.stdout) + else: + print(f"Failed to apply VulnerabilityReport:") + print(f"STDERR: {{result.stderr}}") + print(f"STDOUT: {{result.stdout}}") + # Print the YAML for debugging + print("Generated YAML:") + with open("/tmp/report.yaml", "r") as f: + print(f.read()) + +except Exception as e: + print(f"Error applying VulnerabilityReport: {{e}}") + sys.exit(1) + +PYTHON_EOF + +echo "=== Scan completed successfully ===" +''' + ], + 'env': [ + { + 'name': 'GRYPE_CHECK_FOR_APP_UPDATE', + 'value': 'false' + }, + { + 'name': 'GRYPE_DB_AUTO_UPDATE', + 'value': 'true' + }, + { + 'name': 'GRYPE_DB_CACHE_DIR', + 'value': '/tmp/grype-db' + } + ], + 'resources': { + 'requests': { + 'cpu': '500m', + 'memory': '512Mi' + }, + 'limits': { + 'cpu': '1000m', + 'memory': '1Gi' + } + }, + 'volumeMounts': [ + { + 'name': 'tmp', + 'mountPath': '/tmp' + } + ], + 'securityContext': { + 'allowPrivilegeEscalation': False, + 'runAsNonRoot': False, + 'runAsUser': 0, + 'capabilities': { + 'drop': ['ALL'] + } + } + }], + 'volumes': [ + { + 'name': 'tmp', + 'emptyDir': {} + } + ], + 'securityContext': { + 'fsGroup': 0 + } + } + } + } + } + + try: + batch_api.create_namespaced_job(namespace, job) + logger.info(f"Successfully created scan job: {namespace}/{job_name} for image {image}") + except kubernetes.client.exceptions.ApiException as e: + if e.status == 409: + logger.info(f"Job {namespace}/{job_name} already exists") + else: + logger.error(f"Failed to create job {namespace}/{job_name}: {e}") + logger.error(f"Response body: {e.body}") + except Exception as e: + logger.error(f"Unexpected error creating job: {e}") + +# Add a probe endpoint for health checks +@kopf.on.probe(id='health') +def health_probe(**kwargs): + return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} \ No newline at end of file diff --git a/grype-operator/requirements.txt b/grype-operator/requirements.txt new file mode 100644 index 00000000000..64b5323cc9e --- /dev/null +++ b/grype-operator/requirements.txt @@ -0,0 +1,3 @@ +kopf==1.37.2 +kubernetes==28.1.0 +pyyaml==6.0.1 \ No newline at end of file