From bbc4fef0e870bb23aa81486259f045dc0cb2f52f Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Sat, 10 May 2025 18:16:08 +0200 Subject: [PATCH] feat(trivy): add Trivy integration with Kubernetes, including cron job and email reporting --- ansible/playbook_trivy.yml | 28 +++ inventory/hosts | 3 + terraform/k8sapps/apps.tf | 14 ++ terraform/k8sapps/config.tf | 3 + terraform/modules/apps/sftpgo/variables.tf | 5 +- terraform/modules/utils/trivy/config.tf | 7 + terraform/modules/utils/trivy/main.tf | 186 ++++++++++++++++++ .../utils/trivy/resources/send_report.py | 62 ++++++ terraform/modules/utils/trivy/variables.tf | 59 ++++++ 9 files changed, 363 insertions(+), 4 deletions(-) create mode 100644 ansible/playbook_trivy.yml create mode 100644 terraform/modules/utils/trivy/config.tf create mode 100644 terraform/modules/utils/trivy/main.tf create mode 100644 terraform/modules/utils/trivy/resources/send_report.py create mode 100644 terraform/modules/utils/trivy/variables.tf diff --git a/ansible/playbook_trivy.yml b/ansible/playbook_trivy.yml new file mode 100644 index 0000000..710ae1d --- /dev/null +++ b/ansible/playbook_trivy.yml @@ -0,0 +1,28 @@ +--- +- name: Trivy Playbook (K8s) + hosts: trivy + gather_facts: true + +# Specific tasks for this playbook + tasks: + - name: Set required facts + ansible.builtin.set_fact: + trivy_home: "{{ persistent_data | default('/var/lib') }}/trivy" + trivy_user_id: 1000 + trivy_group_id: 1000 + tags: always + + - name: Create required trivy data folder + become: true + ansible.builtin.file: + state: directory + path: "{{ item }}" + mode: "0750" + owner: "{{ trivy_user_id }}" + group: "{{ trivy_group_id }}" + with_items: + - "{{ trivy_home }}" + - "{{ trivy_home }}/cache" + tags: + - tasks + - tasks::folders diff --git a/inventory/hosts b/inventory/hosts index dae9c0c..28b0904 100644 --- a/inventory/hosts +++ b/inventory/hosts @@ -45,3 +45,6 @@ prod-01.alexpires.me [web] prod-01.alexpires.me + +[trivy] +prod-01.alexpires.me diff --git a/terraform/k8sapps/apps.tf b/terraform/k8sapps/apps.tf index 469ebb5..de9ee1a 100644 --- a/terraform/k8sapps/apps.tf +++ b/terraform/k8sapps/apps.tf @@ -114,3 +114,17 @@ module "sftpgo" { access_key = scaleway_iam_api_key.sftpgo_app_api.access_key secret_key = scaleway_iam_api_key.sftpgo_app_api.secret_key } + +module "trivy" { + source = "../modules/utils/trivy" + + persistent_folder = local.persistent_folder + cron_schedule = local.trivy_cronjob_schedule + + smtp_host = local.scaleway_smtp_host + smtp_port = local.scaleway_smtp_port + smtp_username = var.scaleway_project_id + smtp_password = scaleway_iam_api_key.mail_app_api.secret_key + from_email = "trivy@${local.primary_domain}" + to_email = "c.alexandre.pires@${local.primary_domain}" +} diff --git a/terraform/k8sapps/config.tf b/terraform/k8sapps/config.tf index aa1860c..a70c667 100644 --- a/terraform/k8sapps/config.tf +++ b/terraform/k8sapps/config.tf @@ -39,6 +39,9 @@ locals { rustdesk_fqdn = "rustdesk.${local.primary_domain}" + # trivy + trivy_cronjob_schedule = "0 1 1/15 * *" + # backup local_backup_retention_days = 2 cloud_backup_retention_days = 3 diff --git a/terraform/modules/apps/sftpgo/variables.tf b/terraform/modules/apps/sftpgo/variables.tf index 49ff9ad..e66b343 100644 --- a/terraform/modules/apps/sftpgo/variables.tf +++ b/terraform/modules/apps/sftpgo/variables.tf @@ -1,10 +1,8 @@ - variable "persistent_folder" { description = "The path to the persistent folder" type = string } - variable "backup_folder" { description = "The path to the backup folder" type = string @@ -54,7 +52,6 @@ variable "from_email" { description = "The email address to use as the sender" type = string } - variable "access_key" { description = "The access key for the S3 compatible storage" type = string @@ -83,4 +80,4 @@ variable "tls_enabled" { type = bool default = true } - + diff --git a/terraform/modules/utils/trivy/config.tf b/terraform/modules/utils/trivy/config.tf new file mode 100644 index 0000000..0423411 --- /dev/null +++ b/terraform/modules/utils/trivy/config.tf @@ -0,0 +1,7 @@ +locals { + cache_path = "${var.persistent_folder}/trivy/cache" + cronjob_schedule = var.cron_schedule + python_version = "3.12-alpine3.21" + + send_report_script = file("${path.module}/resources/send_report.py") +} diff --git a/terraform/modules/utils/trivy/main.tf b/terraform/modules/utils/trivy/main.tf new file mode 100644 index 0000000..49cdf43 --- /dev/null +++ b/terraform/modules/utils/trivy/main.tf @@ -0,0 +1,186 @@ +resource "kubernetes_namespace" "trivy" { + metadata { + name = "trivy" + } +} + +resource "kubernetes_service_account" "trivy" { + metadata { + name = "trivy" + namespace = kubernetes_namespace.trivy.metadata[0].name + } + + automount_service_account_token = false +} + +resource "kubernetes_cluster_role" "trivy" { + metadata { + name = "trivy" + } + rule { + api_groups = [""] + resources = ["*"] + verbs = ["list"] + } + rule { + api_groups = ["apps", "batch", "networking.k8s.io", "rbac.authorization.k8s.io"] + resources = ["*"] + verbs = ["list"] + } +} + +resource "kubernetes_cluster_role_binding" "trivy" { + metadata { + name = "trivy" + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.trivy.metadata[0].name + } + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.trivy.metadata[0].name + namespace = kubernetes_namespace.trivy.metadata[0].name + } +} + +resource "kubernetes_secret" "email_credentials" { + metadata { + name = "trivy-email-credentials" + namespace = kubernetes_namespace.trivy.metadata[0].name + } + data = { + smtp_username = var.smtp_username + smtp_password = var.smtp_password + } +} + +resource "kubernetes_config_map" "send_report" { + metadata { + name = "trivy-send-report" + namespace = kubernetes_namespace.trivy.metadata[0].name + } + data = { + "send_report.py" = local.send_report_script + } +} + +resource "kubernetes_cron_job_v1" "trivy" { + metadata { + name = "trivy-scan" + namespace = kubernetes_namespace.trivy.metadata[0].name + } + spec { + schedule = var.cron_schedule + concurrency_policy = "Forbid" + successful_jobs_history_limit = 1 + failed_jobs_history_limit = 1 + job_template { + metadata { + name = "trivy-scan" + } + spec { + backoff_limit = 1 + parallelism = 1 + completions = 1 + active_deadline_seconds = 250 + + template { + metadata { + labels = { + app = "trivy-scan" + } + } + spec { + restart_policy = "Never" + service_account_name = kubernetes_service_account.trivy.metadata[0].name + automount_service_account_token = true + container { + image = "aquasec/trivy:${var.tag}" + name = "trivy" + args = ["k8s", "--report=${var.report_type}", "--exit-code=0", "--severity=${var.levels}", "--cache-dir=/cache", "-o", "/reports/trivy.txt", "--disable-node-collector"] + volume_mount { + name = "trivy-reports" + mount_path = "/reports" + } + volume_mount { + name = "trivy-cache" + mount_path = "/cache" + } + } + container { + name = "send-report" + image = "python:${local.python_version}" + args = ["/scripts/send_report.py"] + env { + name = "REPORT_PATH" + value = "/reports/trivy.txt" + } + env { + name = "SMTP_HOST" + value = var.smtp_host + } + env { + name = "SMTP_PORT" + value = var.smtp_port + } + env { + name = "SMTP_USER" + value_from { + secret_key_ref { + key = "smtp_username" + name = kubernetes_secret.email_credentials.metadata[0].name + } + } + } + env { + name = "SMTP_PASS" + value_from { + secret_key_ref { + key = "smtp_password" + name = kubernetes_secret.email_credentials.metadata[0].name + } + } + } + env { + name = "EMAIL_FROM" + value = var.from_email + } + env { + name = "EMAIL_TO" + value = var.to_email + } + volume_mount { + name = "trivy-reports" + mount_path = "/reports" + } + volume_mount { + name = "trivy-scripts" + mount_path = "/scripts" + } + } + volume { + name = "trivy-reports" + empty_dir {} + } + volume { + name = "trivy-cache" + host_path { + path = local.cache_path + } + } + volume { + name = "trivy-scripts" + config_map { + name = kubernetes_config_map.send_report.metadata[0].name + default_mode = "0755" + + } + } + } + } + } + } + } +} diff --git a/terraform/modules/utils/trivy/resources/send_report.py b/terraform/modules/utils/trivy/resources/send_report.py new file mode 100644 index 0000000..63f6581 --- /dev/null +++ b/terraform/modules/utils/trivy/resources/send_report.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import os +import time +import smtplib +from email.message import EmailMessage + +REPORT_PATH = os.environ.get("REPORT_PATH", "/tmp/trivy_report.txt") +WAIT_TIMEOUT = int(os.environ.get("WAIT_TIMEOUT", "200")) +SLEEP_INTERVAL = int(os.environ.get("SLEEP_INTERVAL", "5") ) + +def dispatch_email(report_path, subject): + msg = EmailMessage() + msg["Subject"] = subject + msg["From"] = os.environ.get("EMAIL_FROM", "noreply@example.com") + msg["To"] = os.environ.get("EMAIL_TO", "admin@example.com") + + if report_path: + msg.set_content("Trivy scan report attached.") + + # Attach the report file + with open(report_path, "rb") as f: + file_data = f.read() + file_name = os.path.basename(report_path) + msg.add_attachment(file_data, maintype="text", subtype="plain", filename=file_name) + else: + msg.set_content("Trivy scan report not found. Check logs for details.") + + smtp_host = os.environ.get("SMTP_HOST", "smtp.example.com") + smtp_port = int(os.environ.get("SMTP_PORT", "587")) + smtp_user = os.environ.get("SMTP_USER", "user") + smtp_pass = os.environ.get("SMTP_PASS", "pass") + + with smtplib.SMTP(smtp_host, smtp_port) as server: + if smtp_port == 465: + server = smtplib.SMTP_SSL(smtp_host, smtp_port) + else: + server = smtplib.SMTP(smtp_host, smtp_port) + if smtp_port != 465: + server.starttls() + try: + server.login(smtp_user, smtp_pass) + except smtplib.SMTPAuthenticationError: + print("SMTP Authentication Error: Check your SMTP credentials.") + return + server.send_message(msg) + print("Report sent.") + +print("Trivy report path:", REPORT_PATH) +print("Starting to wait for the report to be generated...") +start = time.time() +while not os.path.exists(REPORT_PATH): + if time.time() - start > WAIT_TIMEOUT: + print("Timeout waiting for report to be generated.") + dispatch_email(None, "Trivy Scan Report - Timeout") + exit(1) + print("Still waiting for report to be generated...") + time.sleep(SLEEP_INTERVAL) + +print("Report generated, proceeding to send email...") +time.sleep(5) # Wait for a bit to ensure the file is fully written + +dispatch_email(REPORT_PATH, "Trivy Scan Report - " + time.strftime("%Y-%m-%d %H:%M:%S")) diff --git a/terraform/modules/utils/trivy/variables.tf b/terraform/modules/utils/trivy/variables.tf new file mode 100644 index 0000000..c59ee3c --- /dev/null +++ b/terraform/modules/utils/trivy/variables.tf @@ -0,0 +1,59 @@ +variable "persistent_folder" { + description = "The path to the persistent folder" + type = string +} + +variable "smtp_host" { + description = "The SMTP relay host" + type = string +} + +variable "smtp_port" { + description = "The SMTP relay port" + type = number +} + +variable "smtp_username" { + description = "The SMTP relay username" + type = string + sensitive = true +} + +variable "smtp_password" { + description = "The SMTP relay password" + type = string + sensitive = true +} + +variable "from_email" { + description = "The email address to use as the sender" + type = string +} + +variable "to_email" { + description = "The email address to send the report to" + type = string +} + +variable "cron_schedule" { + description = "Cronjob schedule for data backups" + type = string +} + +variable "tag" { + description = "The version of Trivy to install" + type = string + default = "latest" +} + +variable "report_type" { + description = "The type of report to generate" + type = string + default = "all" +} + +variable "levels" { + description = "The severity levels to include in the report" + type = string + default = "HIGH,CRITICAL" +}