From 63ad19f7f17658bf3df143059cf28e4507abf17a Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Sun, 9 Nov 2025 22:14:30 +0530 Subject: [PATCH 01/10] first commit for grype k8s operator --- grype-operator/Dockerfile | 27 + grype-operator/crd.yaml | 136 +++++ grype-operator/deployment.yaml | 196 +++++++ grype-operator/operator.py | 892 ++++++++++++++++++++++++++++++++ grype-operator/requirements.txt | 3 + 5 files changed, 1254 insertions(+) create mode 100644 grype-operator/Dockerfile create mode 100644 grype-operator/crd.yaml create mode 100644 grype-operator/deployment.yaml create mode 100644 grype-operator/operator.py create mode 100644 grype-operator/requirements.txt diff --git a/grype-operator/Dockerfile b/grype-operator/Dockerfile new file mode 100644 index 00000000000..6e3948d65cd --- /dev/null +++ b/grype-operator/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.11-slim + +# Install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install kubectl +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 + +# Set working directory +WORKDIR /app + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy operator code +COPY operator.py . + +# Run as root (not recommended for production, but works for testing) +# Run the operator using kopf CLI with standalone mode +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/crd.yaml b/grype-operator/crd.yaml new file mode 100644 index 00000000000..e81764ac9e3 --- /dev/null +++ b/grype-operator/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/deployment.yaml b/grype-operator/deployment.yaml new file mode 100644 index 00000000000..1f49968de60 --- /dev/null +++ b/grype-operator/deployment.yaml @@ -0,0 +1,196 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: grype-operator-system + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: grype-operator + namespace: grype-operator-system + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: grype-scanner + namespace: grype-operator-system + +--- + +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 + + + +--- +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: ttl.sh/grype-rohan:24h + 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: + # allowPrivilegeEscalation: false + # runAsNonRoot: true + # runAsUser: 1000 + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} + # securityContext: + # fsGroup: 1000 + # runAsNonRoot: true + # seccompProfile: + # type: RuntimeDefault + +--- +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" + +--- +# Create scanner ServiceAccount in default namespace +apiVersion: v1 +kind: ServiceAccount +metadata: + name: grype-scanner + namespace: default + +--- +# 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..c19e584cb62 --- /dev/null +++ b/grype-operator/operator.py @@ -0,0 +1,892 @@ +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()} + + + + + + + + + + + + + + +# #!/usr/bin/env python3 +# """ +# Grype Kubernetes Operator +# Creates scanning jobs for container images in Kubernetes workloads +# """ + +# 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 grype using official method +# echo "Installing Grype..." +# apk add --no-cache curl +# 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 From 43bff40425b1562b83160bc2a1b47f9dc9169a28 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Sun, 9 Nov 2025 22:15:39 +0530 Subject: [PATCH 02/10] modified dockerfile --- grype-operator/Dockerfile | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/grype-operator/Dockerfile b/grype-operator/Dockerfile index 6e3948d65cd..7ea64079f04 100644 --- a/grype-operator/Dockerfile +++ b/grype-operator/Dockerfile @@ -1,27 +1,17 @@ FROM python:3.11-slim - -# Install system dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ ca-certificates \ && rm -rf /var/lib/apt/lists/* - -# Install kubectl 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 # Set working directory WORKDIR /app - -# Copy requirements and install Python dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt - -# Copy operator code COPY operator.py . - -# Run as root (not recommended for production, but works for testing) # Run the operator using kopf CLI with standalone mode CMD ["kopf", "run", "/app/operator.py", "--verbose", "--standalone", "--liveness=http://0.0.0.0:8080/healthz"] \ No newline at end of file From 96cdcaed773de938cd990fc8e05322da2000c356 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Sun, 9 Nov 2025 22:16:14 +0530 Subject: [PATCH 03/10] removed comments --- grype-operator/operator.py | 455 +------------------------------------ 1 file changed, 1 insertion(+), 454 deletions(-) diff --git a/grype-operator/operator.py b/grype-operator/operator.py index c19e584cb62..fa91d5971f3 100644 --- a/grype-operator/operator.py +++ b/grype-operator/operator.py @@ -436,457 +436,4 @@ def create_scan_job(namespace: str, pod_name: str, pod_uid: str, # Add a probe endpoint for health checks @kopf.on.probe(id='health') def health_probe(**kwargs): - return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} - - - - - - - - - - - - - - -# #!/usr/bin/env python3 -# """ -# Grype Kubernetes Operator -# Creates scanning jobs for container images in Kubernetes workloads -# """ - -# 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 grype using official method -# echo "Installing Grype..." -# apk add --no-cache curl -# 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 + return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} \ No newline at end of file From 14e4c04e18c4e7a1f4b739d82c4ffc14f048b1e8 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Sun, 9 Nov 2025 22:20:50 +0530 Subject: [PATCH 04/10] modified the deployment file and dockerfile --- grype-operator/Dockerfile | 1 - grype-operator/deployment.yaml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/grype-operator/Dockerfile b/grype-operator/Dockerfile index 7ea64079f04..f23d632b819 100644 --- a/grype-operator/Dockerfile +++ b/grype-operator/Dockerfile @@ -7,7 +7,6 @@ RUN apt-get update && \ 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 - # Set working directory WORKDIR /app COPY requirements.txt . diff --git a/grype-operator/deployment.yaml b/grype-operator/deployment.yaml index 1f49968de60..ae8feb52178 100644 --- a/grype-operator/deployment.yaml +++ b/grype-operator/deployment.yaml @@ -126,7 +126,7 @@ spec: limits: cpu: 200m memory: 256Mi - # securityContext: + # securityContext: I commented these as they might cause issues in some environments # allowPrivilegeEscalation: false # runAsNonRoot: true # runAsUser: 1000 @@ -140,7 +140,7 @@ spec: volumes: - name: tmp emptyDir: {} - # securityContext: + # securityContext: I commented these as they might cause issues in some environments # fsGroup: 1000 # runAsNonRoot: true # seccompProfile: From 4ea95a137fb0d1086a589a7b5f4998acfc752554 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Sun, 9 Nov 2025 22:22:34 +0530 Subject: [PATCH 05/10] adding readme file --- grype-operator/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 grype-operator/README.md diff --git a/grype-operator/README.md b/grype-operator/README.md new file mode 100644 index 00000000000..e69de29bb2d From 0f4017c9abd649a799f6cee70fbe11c9c30ccaee Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Mon, 10 Nov 2025 17:20:03 +0530 Subject: [PATCH 06/10] some improvements --- grype-operator/Dockerfile | 2 - grype-operator/deployment.yaml | 196 ------------------- grype-operator/manifests/configmap.yaml | 23 +++ grype-operator/{ => manifests}/crd.yaml | 0 grype-operator/manifests/deployment.yaml | 74 +++++++ grype-operator/manifests/namespace.yaml | 4 + grype-operator/manifests/rbac.yaml | 48 +++++ grype-operator/manifests/serviceaccount.yaml | 22 +++ 8 files changed, 171 insertions(+), 198 deletions(-) delete mode 100644 grype-operator/deployment.yaml create mode 100644 grype-operator/manifests/configmap.yaml rename grype-operator/{ => manifests}/crd.yaml (100%) create mode 100644 grype-operator/manifests/deployment.yaml create mode 100644 grype-operator/manifests/namespace.yaml create mode 100644 grype-operator/manifests/rbac.yaml create mode 100644 grype-operator/manifests/serviceaccount.yaml diff --git a/grype-operator/Dockerfile b/grype-operator/Dockerfile index f23d632b819..642537354fd 100644 --- a/grype-operator/Dockerfile +++ b/grype-operator/Dockerfile @@ -7,10 +7,8 @@ RUN apt-get update && \ 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 -# Set working directory WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY operator.py . -# Run the operator using kopf CLI with standalone mode 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/deployment.yaml b/grype-operator/deployment.yaml deleted file mode 100644 index ae8feb52178..00000000000 --- a/grype-operator/deployment.yaml +++ /dev/null @@ -1,196 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: grype-operator-system - ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: grype-operator - namespace: grype-operator-system - ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: grype-scanner - namespace: grype-operator-system - ---- - -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 - - - ---- -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: ttl.sh/grype-rohan:24h - 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 - # 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 - # fsGroup: 1000 - # runAsNonRoot: true - # seccompProfile: - # type: RuntimeDefault - ---- -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" - ---- -# Create scanner ServiceAccount in default namespace -apiVersion: v1 -kind: ServiceAccount -metadata: - name: grype-scanner - namespace: default - ---- -# 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/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/crd.yaml b/grype-operator/manifests/crd.yaml similarity index 100% rename from grype-operator/crd.yaml rename to grype-operator/manifests/crd.yaml diff --git a/grype-operator/manifests/deployment.yaml b/grype-operator/manifests/deployment.yaml new file mode 100644 index 00000000000..21640d90857 --- /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: ttl.sh/grype-rohan:24h + 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 From 5a8da303acbe1c14554953acb9338dc826cc0f97 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi Date: Mon, 10 Nov 2025 17:22:57 +0530 Subject: [PATCH 07/10] starting readme documentation stuff --- grype-operator/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grype-operator/README.md b/grype-operator/README.md index e69de29bb2d..f2e59066c84 100644 --- a/grype-operator/README.md +++ b/grype-operator/README.md @@ -0,0 +1,2 @@ +## Full Demo Video for Grype Operator for Kubernetes Clusters + From ad718ab79fb87fe8fdd2d2154afe33aa1c6dfe3d Mon Sep 17 00:00:00 2001 From: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:29:02 +0530 Subject: [PATCH 08/10] demo video Signed-off-by: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> --- grype-operator/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grype-operator/README.md b/grype-operator/README.md index f2e59066c84..0ab2a64e6cf 100644 --- a/grype-operator/README.md +++ b/grype-operator/README.md @@ -1,2 +1,5 @@ ## Full Demo Video for Grype Operator for Kubernetes Clusters + +https://github.com/user-attachments/assets/2dd85c3b-b30c-42c5-b589-efecd4423ab1 + From 5f0fc53f977cc7038fb28fc5d007493020fb52fd Mon Sep 17 00:00:00 2001 From: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:04:53 +0530 Subject: [PATCH 09/10] Enhance README with installation and testing details Added detailed installation instructions and testing steps for the Grype operator, including pre-requisites and example commands. Signed-off-by: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> --- grype-operator/README.md | 144 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/grype-operator/README.md b/grype-operator/README.md index 0ab2a64e6cf..e6092728432 100644 --- a/grype-operator/README.md +++ b/grype-operator/README.md @@ -1,3 +1,147 @@ +## 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 From 5a4d5a7d6b5ffa3d8b985a08d392aefb8908ff76 Mon Sep 17 00:00:00 2001 From: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> Date: Mon, 10 Nov 2025 18:11:33 +0530 Subject: [PATCH 10/10] Update Grype operator image to latest version Signed-off-by: Rohan Rustagi <110477025+RohanRusta21@users.noreply.github.com> --- grype-operator/manifests/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grype-operator/manifests/deployment.yaml b/grype-operator/manifests/deployment.yaml index 21640d90857..af65d2e5013 100644 --- a/grype-operator/manifests/deployment.yaml +++ b/grype-operator/manifests/deployment.yaml @@ -19,7 +19,7 @@ spec: serviceAccountName: grype-operator containers: - name: operator - image: ttl.sh/grype-rohan:24h + image: rohanrustagi18/grype-operator:latest imagePullPolicy: IfNotPresent env: - name: OPERATOR_NAMESPACE