"
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/automation/secrets-manager.tf b/infrastructure/secrets-finder/scheduled-scans/aws/automation/secrets-manager.tf
new file mode 100644
index 0000000..375f3f6
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/automation/secrets-manager.tf
@@ -0,0 +1,57 @@
+data "aws_secretsmanager_secret" "token_reference_github_organization_hosting_secrets_finder" {
+ count = var.token_reference_github_organization_hosting_secrets_finder != null ? 1 : 0
+ name = var.token_reference_github_organization_hosting_secrets_finder
+}
+
+data "aws_secretsmanager_secret" "datadog_api_key" {
+ count = var.enable_datadog_monitors == true ? 1 : 0
+ name = var.datadog_api_key_reference
+
+ lifecycle {
+ precondition {
+ condition = var.datadog_api_key_reference != null
+ error_message = "Datadog monitors should be set up, but no secret reference to Secrets Manager was provided using the 'datadog_api_key_reference' variable."
+ }
+ }
+}
+
+data "aws_secretsmanager_secret_version" "datadog_api_key" {
+ count = var.enable_datadog_monitors ? 1 : 0
+ secret_id = data.aws_secretsmanager_secret.datadog_api_key[0].id
+
+ lifecycle {
+ precondition {
+ condition = var.datadog_api_key_reference != null
+ error_message = "Datadog monitors should be set up, but no secret reference to Secrets Manager was provided using the 'datadog_api_key_reference' variable."
+ }
+ }
+}
+
+data "aws_secretsmanager_secret" "datadog_application_key" {
+ count = var.enable_datadog_monitors == true ? 1 : 0
+ name = var.datadog_application_key_reference
+
+ lifecycle {
+ precondition {
+ condition = var.datadog_application_key_reference != null
+ error_message = "Datadog monitors should be set up, but no secret reference to Secrets Manager was provided using the 'datadog_application_key_reference' variable."
+ }
+ }
+}
+
+data "aws_secretsmanager_secret_version" "datadog_application_key" {
+ count = var.enable_datadog_monitors ? 1 : 0
+ secret_id = data.aws_secretsmanager_secret.datadog_application_key[0].id
+
+ lifecycle {
+ precondition {
+ condition = var.datadog_application_key_reference != null
+ error_message = "Datadog monitors should be set up, but no secret reference to Secrets Manager was provided using the 'datadog_application_key_reference' variable."
+ }
+ }
+}
+
+data "aws_secretsmanager_secret" "credentials_references" {
+ for_each = toset(local.all_credentials_references)
+ name = each.value
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/automation/sns.tf b/infrastructure/secrets-finder/scheduled-scans/aws/automation/sns.tf
new file mode 100644
index 0000000..f19ec6c
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/automation/sns.tf
@@ -0,0 +1,11 @@
+resource "aws_sns_topic" "important_notifications" {
+ count = var.sns_topic_receiver != null ? 1 : 0
+ name = var.project_name
+}
+
+resource "aws_sns_topic_subscription" "email_subscription" {
+ count = var.sns_topic_receiver != null ? 1 : 0
+ topic_arn = aws_sns_topic.important_notifications[0].arn
+ protocol = "email"
+ endpoint = var.sns_topic_receiver
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/automation/sts.tf b/infrastructure/secrets-finder/scheduled-scans/aws/automation/sts.tf
new file mode 100644
index 0000000..8fc4b38
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/automation/sts.tf
@@ -0,0 +1 @@
+data "aws_caller_identity" "current" {}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/automation/variables.tf b/infrastructure/secrets-finder/scheduled-scans/aws/automation/variables.tf
new file mode 100644
index 0000000..28b29f7
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/automation/variables.tf
@@ -0,0 +1,318 @@
+variable "aws_region" {
+ type = string
+ default = "us-east-1"
+ description = "AWS region where to deploy resources"
+
+ validation {
+ condition = can(regex("^(af|ap|ca|eu|me|sa|us)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\\d+$", var.aws_region))
+ error_message = "You should enter a valid AWS region (https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html)"
+ }
+}
+
+variable "aws_profile" {
+ type = string
+ default = "default"
+ description = "AWS profile to use for authentication"
+}
+
+variable "environment_type" {
+ type = string
+ default = "PRODUCTION"
+ description = "Environment (PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB)"
+
+ validation {
+ condition = contains(["PRODUCTION", "PRE-PRODUCTION", "QUALITY ASSURANCE", "INTEGRATION TESTING", "DEVELOPMENT", "LAB"], var.environment_type)
+ error_message = "The environment type should be one of the following values: PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB (case sensitive)"
+ }
+}
+
+variable "tags" {
+ type = map(string)
+ description = "A map of tags to add to the resources"
+ default = {}
+
+ validation {
+ condition = alltrue([for v in values(var.tags) : v != ""])
+ error_message = "Tag values must not be empty."
+ }
+}
+
+variable "permissions_boundary_arn" {
+ type = string
+ default = null
+ description = "The name of the IAM permissions boundary to attach to the IAM roles created by the module"
+
+ validation {
+ condition = var.permissions_boundary_arn == null || can(regex("^arn:aws:iam::[0-9]{12}:policy\\/([a-zA-Z0-9-_.]+)$", var.permissions_boundary_arn))
+ error_message = "The provided ARN is not a valid ARN for a policy"
+ }
+}
+
+variable "iam_role_path" {
+ type = string
+ default = "/"
+ description = "The path to use when creating IAM roles"
+
+ validation {
+ condition = can(regex("^\\/([a-zA-Z0-9]+([-a-zA-Z0-9]*[a-zA-Z0-9]+)?\\/)*$", var.iam_role_path))
+ error_message = "The provided path is invalid"
+ }
+}
+
+variable "project_name" {
+ type = string
+ default = "secrets-finder"
+ description = "Name of the project (should be the same across all modules of secrets-finder to ensure consistency)"
+}
+
+variable "vpc_name" {
+ type = string
+ description = "Identifier of the VPC to use for secrets-finder"
+}
+
+variable "subnet_name" {
+ type = string
+ description = "Name of the subnet where to deploy the resources (wildcards are allowed: first match is used)"
+}
+
+
+variable "s3_bucket_name" {
+ type = string
+ description = "Name of the S3 bucket containing files used for secrets detection scans"
+}
+
+variable "s3_bucket_remote_states" {
+ type = string
+ description = "Name of the S3 bucket containing the remote states of the infrastructure"
+
+}
+
+variable "dynamodb_table_remote_states" {
+ type = string
+ description = "Name of the DynamoDB table containing the locks used for the remote states representing the infrastructure"
+}
+
+variable "start_schedule" {
+ type = string
+ default = "cron(0 6 ? * MON *)"
+ description = "The cron specifying when a new scanning instance should be set up (default is: every Monday at 06:00, expected format: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions)"
+}
+
+variable "token_reference_github_organization_hosting_secrets_finder" {
+ type = string
+ default = null
+ description = "Name of the secret stored in Secrets Manager containing the GitHub token for the organization hosting the secrets-finder code. Leave empty if the repository is publicly accessible."
+
+ validation {
+ condition = var.token_reference_github_organization_hosting_secrets_finder == null || can(regex("^[a-zA-Z0-9/_+=.@-]{1,512}$", var.token_reference_github_organization_hosting_secrets_finder))
+ error_message = "The secret name is invalid"
+ }
+}
+
+variable "github_organization_hosting_secrets_finder" {
+ type = string
+ description = "Name of the GitHub Organization where the repository containing the secrets-finder code is hosted"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9-]{1,38}$", var.github_organization_hosting_secrets_finder))
+ error_message = "The GitHub organization name must start with a letter or number, can include dashes, and be between 1 and 39 characters."
+ }
+}
+
+variable "github_repository_hosting_secrets_finder" {
+ type = string
+ description = "Name of the GitHub Repository containing the secrets-finder code"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9_.-]{1,100}$", var.github_repository_hosting_secrets_finder))
+ error_message = "The GitHub repository name must be between 1 and 100 characters, and can include letters, numbers, underscores, periods, and dashes."
+ }
+}
+
+variable "sns_topic_receiver" {
+ type = string
+ default = null
+ description = "Email address of the receiver of the SNS topic to which important notifications are sent. Leave empty if no notifications should be sent."
+
+ validation {
+ condition = var.sns_topic_receiver == null || can(regex("^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])$", var.sns_topic_receiver))
+ error_message = "The email address of the receiver is invalid."
+ }
+}
+
+variable "ebs_encryption_key_arn" {
+ type = string
+ default = null
+ description = "The ARN of the KMS key used to encrypt the EBS volumes"
+}
+
+variable "ami_encryption_key_arn" {
+ type = string
+ default = null
+ description = "The ARN of the KMS key used to decrypt/encrypt the AMI used for the scanning instances"
+}
+
+variable "terraform_version" {
+ type = string
+ default = "1.8.5"
+ description = "Version of Terraform to use when starting a new scan from CodeBuild"
+
+ validation {
+ condition = can(regex("^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$", var.terraform_version))
+ error_message = "The Terraform version should be in the format 'x.y.z'"
+ }
+}
+
+variable "trufflehog_configuration_file" {
+ type = string
+ default = null
+ description = "Path to the Trufflehog configuration file. Leave empty if no configuration file should be used."
+
+ validation {
+ condition = var.trufflehog_configuration_file == null || can(fileexists(var.trufflehog_configuration_file))
+ error_message = "The Trufflehog configuration file must exist."
+ }
+
+}
+
+variable "instance_user" {
+ type = string
+ default = "secrets-finder"
+ description = "Username to create and use on the instances started for the scanning process"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", var.instance_user))
+ error_message = "instance_user must contain only alphanumeric characters, dashes, and underscores, and must not start or end with a dash or underscore."
+ }
+}
+
+variable "scans" {
+ description = "List of scans to perform"
+ type = list(object({
+ identifier = string
+ scm = string
+ credentials_reference = string
+ ec2_instance_type = string
+ files = optional(list(string))
+ repositories_to_scan = optional(string)
+ terminate_instance_on_error = optional(bool)
+ terminate_instance_after_scan = optional(bool)
+ report_only_verified = optional(bool)
+ }))
+
+ validation {
+ condition = length(var.scans) > 0
+ error_message = "The scans list must be defined and not empty."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", scan.identifier))
+ ])
+ )
+ error_message = "The identifier field must contain only alphanumeric characters, dashes, and underscores, and must not start or end with a dash or underscore."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : contains(["github", "azure_devops", "custom"], scan.scm)
+ ])
+ )
+ error_message = "The scm field must be one of 'github', 'azure_devops', 'custom'."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : length(scan.credentials_reference) > 0
+ ])
+ )
+ error_message = "Credentials reference must not be empty."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : scan.files == null ? true : alltrue([for file in scan.files : try(fileexists(file), false)])
+ ])
+ )
+ error_message = "All files in the 'files' field must exist."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : scan.repositories_to_scan == null ? true : fileexists(scan.repositories_to_scan)
+ ])
+ )
+ error_message = "When set, repositories_to_scan should reference an existing file on the local system."
+ }
+
+ validation {
+ condition = (
+ alltrue([
+ for scan in var.scans : contains(jsondecode(file("../../../../../configuration/secrets-finder/aws/aws_ec2_instances.json")), scan.ec2_instance_type)
+ ])
+ )
+ error_message = "The ec2_instance_type field must be a valid AWS EC2 instance type."
+ }
+}
+
+variable "enable_datadog_monitors" {
+ type = bool
+ default = true
+ description = "Define whether Datadog monitors should be set up to monitor the status of the EC2 instances and the Codebuild project. If this variable is set to 'true', both 'datadog_api_key_reference' and 'datadog_application_key_reference' variables should be set, and the corresponding secrets should exist in Parameter Store."
+}
+
+variable "datadog_account" {
+ type = string
+ default = null
+ description = "The name of the Datadog account to which EC2 instance metrics should be reported and where monitors are set up. This variable is only used if 'enable_datadog_monitors' variable is set to 'true'."
+}
+
+variable "datadog_api_key_reference" {
+ type = string
+ default = null
+ description = "Name of the secret stored in Secrets Manager and containing the Datadog API key. Leave empty if Datadog should not be configured."
+
+ validation {
+ condition = (var.datadog_api_key_reference == null) || can(regex("^[a-zA-Z0-9/_+=.@-]{1,512}$", var.datadog_api_key_reference))
+ error_message = "The secret name is invalid"
+ }
+}
+
+variable "datadog_application_key_reference" {
+ type = string
+ default = null
+ description = "Name of the secret stored in Secrets Manager and containing the Datadog application key. Leave empty if Datadog monitors should not be configured."
+
+ validation {
+ condition = (var.datadog_application_key_reference == null) || can(regex("^[a-zA-Z0-9/_+=.@-]{1,512}$", var.datadog_application_key_reference))
+ error_message = "The secret name is invalid"
+ }
+}
+
+variable "datadog_monitors_notify_list" {
+ type = list(string)
+ default = []
+ description = "List of recipients to notify whenever an alert is triggered. The format for each recipient should conform with the official specification (https://docs.datadoghq.com/monitors/notify/#notifications). This list is only considered if 'enable_datadog_monitors' variable is set to 'true'."
+}
+
+variable "datadog_ec2_instance_monitor_ec2_age_limit" {
+ type = number
+ default = 1
+ description = "Time (in hours) to wait before considering an instance in an unhealthy state. Value should be between 1 and 72 and is only considered if 'enable_datadog_monitors' is set to 'true'."
+
+ validation {
+ condition = var.datadog_ec2_instance_monitor_ec2_age_limit >= 1 && var.datadog_ec2_instance_monitor_ec2_age_limit <= 72
+ error_message = "The value should be between 1 and 72 (hours)"
+ }
+}
+
+variable "datadog_tags" {
+ type = list(string)
+ default = []
+ description = "A list of tags for Datadog"
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/automation/vpc.tf b/infrastructure/secrets-finder/scheduled-scans/aws/automation/vpc.tf
new file mode 100644
index 0000000..9103bd4
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/automation/vpc.tf
@@ -0,0 +1,18 @@
+data "aws_vpc" "vpc" {
+ filter {
+ name = "tag:Name"
+ values = [var.vpc_name]
+ }
+}
+
+data "aws_subnets" "selected" {
+ filter {
+ name = "tag:Name"
+ values = [var.subnet_name]
+ }
+
+ filter {
+ name = "available-ip-address-count"
+ values = range(1, 200)
+ }
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/README.md b/infrastructure/secrets-finder/scheduled-scans/aws/scan/README.md
new file mode 100644
index 0000000..565811e
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/README.md
@@ -0,0 +1,80 @@
+# scan
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >=1.7 |
+| [aws](#requirement\_aws) | ~> 5.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | ~> 5.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_iam_instance_profile.ec2_instance_profile](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource |
+| [aws_iam_policy.permissions_for_ec2_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+| [aws_iam_role.ec2_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy_attachment.permissions_for_ec2_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_instance.secrets_finder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource |
+| [aws_security_group.new_security_groups](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
+| [aws_ami.amazon_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
+| [aws_iam_policy_document.ec2_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.policy_document_permissions_for_ec2_instance](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_s3_bucket.secrets_finder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket) | data source |
+| [aws_s3_object.setup](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_object) | data source |
+| [aws_secretsmanager_secret.credentials](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source |
+| [aws_secretsmanager_secret.datadog_api_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) | data source |
+| [aws_security_group.existing_security_groups](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/security_group) | data source |
+| [aws_subnets.selected](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnets) | data source |
+| [aws_vpc.vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [ami\_image\_filter](#input\_ami\_image\_filter) | Filter to use to find the Amazon Machine Image (AMI) to use for the EC2 instance the name can contain wildcards. Only GNU/Linux images are supported. | `string` | `"amzn2-ami-hvm*"` | no |
+| [ami\_owner](#input\_ami\_owner) | Owner of the Amazon Machine Image (AMI) to use for the EC2 instance | `string` | `"amazon"` | no |
+| [aws\_profile](#input\_aws\_profile) | AWS profile to use for authentication | `string` | `"default"` | no |
+| [aws\_region](#input\_aws\_region) | AWS region where to deploy resources | `string` | `"us-east-1"` | no |
+| [credentials\_reference](#input\_credentials\_reference) | Name of the secret stored in Secrets Manager and containing the credentials to use for the scan | `string` | n/a | yes |
+| [datadog\_account](#input\_datadog\_account) | The name of the Datadog account to which EC2 instance metrics should be reported and where monitors are set up. This variable is only used if 'datadog\_enable\_ec2\_instance\_metrics' variable is set to 'true'. | `string` | `null` | no |
+| [datadog\_api\_key\_reference](#input\_datadog\_api\_key\_reference) | Name of the secret stored in Secrets Manager and containing the Datadog API key | `string` | `null` | no |
+| [datadog\_enable\_ec2\_instance\_metrics](#input\_datadog\_enable\_ec2\_instance\_metrics) | Enable the metrics for the EC2 instance in Datadog (should be 'true' if monitors are being used to track the health of the EC2 instance) | `bool` | `true` | no |
+| [environment\_type](#input\_environment\_type) | Environment (PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB) | `string` | `"PRODUCTION"` | no |
+| [existing\_security\_groups](#input\_existing\_security\_groups) | List of names representing existing security groups to add to the EC2 instance | `list(string)` | `[]` | no |
+| [iam\_role\_path](#input\_iam\_role\_path) | The path to use when creating IAM roles | `string` | `"/"` | no |
+| [instance\_type](#input\_instance\_type) | instance\_type must be a valid AWS EC2 instance type. | `string` | `"t3a.medium"` | no |
+| [instance\_user](#input\_instance\_user) | Username to create and use on the instance started for the scanning process | `string` | `"secrets-finder"` | no |
+| [new\_security\_groups](#input\_new\_security\_groups) | Security groups to create (see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | list(object({
name = string,
description = string,
ingress : optional(list(object({
from_port = number,
to_port = number,
protocol = any,
description = optional(string),
cidr_blocks = optional(list(string), []),
ipv6_cidr_blocks = optional(list(string), []),
security_groups = optional(list(string), []),
prefix_list_ids = optional(list(string), [])
})), []),
egress : optional(list(object({
from_port = number,
to_port = number,
protocol = any,
description = optional(string),
cidr_blocks = optional(list(string), []),
ipv6_cidr_blocks = optional(list(string), []),
security_groups = optional(list(string), []),
prefix_list_ids = optional(list(string), [])
})), [])
}))
| `[]` | no |
+| [permissions\_boundary\_arn](#input\_permissions\_boundary\_arn) | The name of the IAM permissions boundary to attach to the IAM role created by the module | `string` | `null` | no |
+| [project\_name](#input\_project\_name) | Name of the project (should be the same across all modules of secrets-finder to ensure consistency) | `string` | `"secrets-finder"` | no |
+| [s3\_bucket\_name](#input\_s3\_bucket\_name) | S3 bucket name where to upload the scripts | `string` | n/a | yes |
+| [scan\_identifier](#input\_scan\_identifier) | Identifier of the scan | `string` | n/a | yes |
+| [scm](#input\_scm) | SCM to use for the scan | `string` | n/a | yes |
+| [sns\_topic\_arn](#input\_sns\_topic\_arn) | ARN of the SNS topic to use for notifications. Leave empty if SNS notifications are not needed. | `string` | `null` | no |
+| [subnet\_name](#input\_subnet\_name) | Identifier of the subnet where to deploy the EC2 instance | `string` | n/a | yes |
+| [tags](#input\_tags) | A map of tags to add to the resources | `map(string)` | `{}` | no |
+| [trufflehog\_processes](#input\_trufflehog\_processes) | Define the number of scanning processes that should be spawned by TruffleHog. WARNING: This may be resource intensive and consume all the host resources. | `number` | `20` | no |
+| [trufflehog\_version](#input\_trufflehog\_version) | Version of TruffleHog to use | `string` | `"3.78.2"` | no |
+| [vpc\_name](#input\_vpc\_name) | Identifier of the VPC to use | `string` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [ec2\_instance\_arn](#output\_ec2\_instance\_arn) | n/a |
+| [ec2\_instance\_id](#output\_ec2\_instance\_id) | n/a |
+| [ec2\_role\_arn](#output\_ec2\_role\_arn) | n/a |
+
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/iam.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/iam.tf
new file mode 100644
index 0000000..a998010
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/iam.tf
@@ -0,0 +1,110 @@
+resource "aws_iam_role" "ec2_role" {
+ name = "${var.project_name}-ec2-role"
+ assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
+ path = var.iam_role_path
+ permissions_boundary = var.permissions_boundary_arn
+}
+
+data "aws_iam_policy_document" "ec2_assume_role" {
+ statement {
+ effect = "Allow"
+ principals {
+ identifiers = ["ec2.amazonaws.com"]
+ type = "Service"
+ }
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+data "aws_iam_policy_document" "policy_document_permissions_for_ec2_instance" {
+ statement {
+ sid = "ListS3Bucket"
+ effect = "Allow"
+ actions = ["s3:ListBucket"]
+ resources = [data.aws_s3_bucket.secrets_finder.arn]
+ }
+
+ statement {
+ sid = "GetAndPutObjectsInS3Bucket"
+ effect = "Allow"
+ actions = [
+ "s3:GetObject*",
+ "s3:PutObject*"
+ ]
+ resources = ["${data.aws_s3_bucket.secrets_finder.arn}/*"]
+ }
+
+ statement {
+ sid = "AccessSecretInSecretsManager"
+ effect = "Allow"
+ actions = [
+ "secretsmanager:GetResourcePolicy",
+ "secretsmanager:DescribeSecret",
+ "secretsmanager:GetSecretValue",
+ ]
+ resources = [data.aws_secretsmanager_secret.credentials.arn]
+ }
+
+ dynamic "statement" {
+ for_each = (var.datadog_api_key_reference != null) ? [var.datadog_api_key_reference] : []
+ content {
+ sid = "FetchDatadogAPIKey"
+ effect = "Allow"
+ actions = [
+ "secretsmanager:GetResourcePolicy",
+ "secretsmanager:DescribeSecret",
+ "secretsmanager:GetSecretValue"
+ ]
+ resources = [data.aws_secretsmanager_secret.datadog_api_key[0].arn]
+ }
+ }
+
+ statement {
+ sid = "AllowTerminationOfEC2Instance"
+ effect = "Allow"
+ actions = [
+ "ec2:TerminateInstances"
+ ]
+ resources = ["arn:aws:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"]
+
+ condition {
+ test = "StringLike"
+ variable = "aws:ResourceTag/Name"
+ values = ["${var.project_name}*"]
+ }
+
+ condition {
+ test = "StringLike"
+ variable = "ec2:InstanceProfile"
+ values = [aws_iam_instance_profile.ec2_instance_profile.arn]
+ }
+ }
+
+ dynamic "statement" {
+ for_each = var.sns_topic_arn != null ? [var.sns_topic_arn] : []
+ content {
+ sid = "AllowToEmitImportantNotifications"
+ effect = "Allow"
+ actions = [
+ "sns:Publish"
+ ]
+ resources = [var.sns_topic_arn]
+ }
+ }
+}
+
+resource "aws_iam_policy" "permissions_for_ec2_instance" {
+ name = "${var.project_name}-ec2-permissions"
+ description = "Policy granting necessary permissions to EC2 instance"
+ policy = data.aws_iam_policy_document.policy_document_permissions_for_ec2_instance.json
+}
+
+resource "aws_iam_role_policy_attachment" "permissions_for_ec2_instance" {
+ policy_arn = aws_iam_policy.permissions_for_ec2_instance.arn
+ role = aws_iam_role.ec2_role.name
+}
+
+resource "aws_iam_instance_profile" "ec2_instance_profile" {
+ name = "${var.project_name}-instance-profile"
+ role = aws_iam_role.ec2_role.name
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/images.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/images.tf
new file mode 100644
index 0000000..ae4a371
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/images.tf
@@ -0,0 +1,9 @@
+data "aws_ami" "amazon_ami" {
+ most_recent = true
+ owners = [var.ami_owner]
+
+ filter {
+ name = "name"
+ values = ["${var.ami_image_filter}"]
+ }
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/locals.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/locals.tf
new file mode 100644
index 0000000..34652b7
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/locals.tf
@@ -0,0 +1,4 @@
+locals {
+ environment = replace(lower(var.environment_type), " ", "-")
+ tags = merge(try(var.tags, {}), { environment = local.environment })
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/main.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/main.tf
new file mode 100644
index 0000000..3c30cf4
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/main.tf
@@ -0,0 +1,38 @@
+resource "aws_instance" "secrets_finder" {
+ ami = data.aws_ami.amazon_ami.id
+ instance_type = var.instance_type
+ subnet_id = data.aws_subnets.selected.ids[0]
+ iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name
+ vpc_security_group_ids = concat([for sg in data.aws_security_group.existing_security_groups : sg.id], [for sg in aws_security_group.new_security_groups : sg.id])
+
+ user_data_replace_on_change = true
+
+ root_block_device {
+ volume_size = 30
+ volume_type = "gp2"
+ delete_on_termination = true
+ }
+
+ user_data = data.aws_s3_object.setup.body
+
+ tags = merge(
+ {
+ Name = "${var.project_name}-${var.scan_identifier}"
+ },
+ [
+ (var.datadog_enable_ec2_instance_metrics == true) ? { datadog-account = var.datadog_account } : null
+ ]...
+ )
+
+ lifecycle {
+ precondition {
+ condition = (var.datadog_enable_ec2_instance_metrics == false) || (var.datadog_enable_ec2_instance_metrics == true && var.datadog_account != null)
+ error_message = "EC2 instance metrics should be enabled but no Datadog account was provided (variable 'datadog_account' has no value)"
+ }
+ }
+
+ depends_on = [
+ aws_iam_policy.permissions_for_ec2_instance,
+ aws_iam_role_policy_attachment.permissions_for_ec2_instance
+ ]
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/outputs.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/outputs.tf
new file mode 100644
index 0000000..ec5953c
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/outputs.tf
@@ -0,0 +1,11 @@
+output "ec2_role_arn" {
+ value = aws_iam_role.ec2_role.arn
+}
+
+output "ec2_instance_id" {
+ value = aws_instance.secrets_finder.id
+}
+
+output "ec2_instance_arn" {
+ value = aws_instance.secrets_finder.arn
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/providers.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/providers.tf
new file mode 100644
index 0000000..d089c28
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/providers.tf
@@ -0,0 +1,20 @@
+terraform {
+ required_version = ">=1.7"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ }
+
+ backend "s3" {
+ encrypt = true
+ }
+}
+
+provider "aws" {
+ region = var.aws_region
+ profile = var.aws_profile
+ default_tags { tags = local.tags }
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tf
new file mode 100644
index 0000000..df05240
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tf
@@ -0,0 +1,8 @@
+data "aws_s3_bucket" "secrets_finder" {
+ bucket = var.s3_bucket_name
+}
+
+data "aws_s3_object" "setup" {
+ bucket = data.aws_s3_bucket.secrets_finder.id
+ key = "secrets-finder/scheduled-scans/scans/${var.scan_identifier}/setup/setup.sh"
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tfbackend b/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tfbackend
new file mode 100644
index 0000000..6fa4016
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/s3.tfbackend
@@ -0,0 +1,5 @@
+bucket = ""
+key = ""
+region = ""
+dynamodb_table = ""
+profile = ""
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/secrets-manager.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/secrets-manager.tf
new file mode 100644
index 0000000..9911575
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/secrets-manager.tf
@@ -0,0 +1,8 @@
+data "aws_secretsmanager_secret" "datadog_api_key" {
+ count = var.datadog_api_key_reference != null ? 1 : 0
+ name = var.datadog_api_key_reference
+}
+
+data "aws_secretsmanager_secret" "credentials" {
+ name = var.credentials_reference
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/security-groups.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/security-groups.tf
new file mode 100644
index 0000000..a2c0b52
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/security-groups.tf
@@ -0,0 +1,43 @@
+data "aws_security_group" "existing_security_groups" {
+ for_each = { for sg in var.existing_security_groups : sg => sg }
+ filter {
+ name = "group-name"
+ values = [each.value]
+ }
+ vpc_id = data.aws_vpc.vpc.id
+}
+
+resource "aws_security_group" "new_security_groups" {
+ for_each = { for sg in var.new_security_groups : sg.name => sg }
+ name = each.value.name
+ description = each.value.description
+ vpc_id = data.aws_vpc.vpc.id
+
+ dynamic "ingress" {
+ for_each = each.value["ingress"]
+ content {
+ from_port = ingress.value.from_port
+ to_port = ingress.value.to_port
+ protocol = ingress.value.protocol
+ cidr_blocks = ingress.value.cidr_blocks
+ description = ingress.value.description
+ ipv6_cidr_blocks = ingress.value.ipv6_cidr_blocks
+ security_groups = ingress.value.security_groups
+ prefix_list_ids = ingress.value.prefix_list_ids
+ }
+ }
+
+ dynamic "egress" {
+ for_each = each.value["egress"]
+ content {
+ from_port = egress.value.from_port
+ to_port = egress.value.to_port
+ protocol = egress.value.protocol
+ cidr_blocks = egress.value.cidr_blocks
+ description = egress.value.description
+ ipv6_cidr_blocks = egress.value.ipv6_cidr_blocks
+ security_groups = egress.value.security_groups
+ prefix_list_ids = egress.value.prefix_list_ids
+ }
+ }
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/sts.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/sts.tf
new file mode 100644
index 0000000..8fc4b38
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/sts.tf
@@ -0,0 +1 @@
+data "aws_caller_identity" "current" {}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/variables.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/variables.tf
new file mode 100644
index 0000000..018b07b
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/variables.tf
@@ -0,0 +1,270 @@
+variable "aws_region" {
+ type = string
+ default = "us-east-1"
+ description = "AWS region where to deploy resources"
+
+ validation {
+ condition = can(regex("^(af|ap|ca|eu|me|sa|us)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\\d+$", var.aws_region))
+ error_message = "You should enter a valid AWS region (https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html)"
+ }
+}
+
+variable "aws_profile" {
+ type = string
+ default = "default"
+ description = "AWS profile to use for authentication"
+}
+
+variable "tags" {
+ type = map(string)
+ description = "A map of tags to add to the resources"
+ default = {}
+
+ validation {
+ condition = alltrue([for v in values(var.tags) : v != ""])
+ error_message = "Tag values must not be empty."
+ }
+}
+
+variable "project_name" {
+ type = string
+ default = "secrets-finder"
+ description = "Name of the project (should be the same across all modules of secrets-finder to ensure consistency)"
+}
+
+variable "environment_type" {
+ type = string
+ default = "PRODUCTION"
+ description = "Environment (PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB)"
+
+ validation {
+ condition = contains(["PRODUCTION", "PRE-PRODUCTION", "QUALITY ASSURANCE", "INTEGRATION TESTING", "DEVELOPMENT", "LAB"], var.environment_type)
+ error_message = "The environment type should be one of the following values: PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB (case sensitive)"
+ }
+}
+
+variable "vpc_name" {
+ type = string
+ description = "Identifier of the VPC to use"
+}
+
+variable "subnet_name" {
+ type = string
+ description = "Identifier of the subnet where to deploy the EC2 instance"
+}
+
+variable "s3_bucket_name" {
+ type = string
+ description = "S3 bucket name where to upload the scripts"
+}
+
+variable "permissions_boundary_arn" {
+ type = string
+ default = null
+ description = "The name of the IAM permissions boundary to attach to the IAM role created by the module"
+
+ validation {
+ condition = var.permissions_boundary_arn == null || can(regex("^arn:aws:iam::[0-9]{12}:policy\\/([a-zA-Z0-9-_.]+)$", var.permissions_boundary_arn))
+ error_message = "The provided ARN is not a valid ARN for a policy"
+ }
+}
+
+variable "iam_role_path" {
+ type = string
+ default = "/"
+ description = "The path to use when creating IAM roles"
+
+ validation {
+ condition = can(regex("^\\/([a-zA-Z0-9]+([-a-zA-Z0-9]*[a-zA-Z0-9]+)?\\/)*$", var.iam_role_path))
+ error_message = "The provided path is invalid"
+ }
+}
+
+variable "scm" {
+ type = string
+ description = "SCM to use for the scan"
+
+ validation {
+ condition = contains(["github", "azure_devops", "custom"], var.scm)
+ error_message = "scm must be one of 'github', 'azure_devops', 'custom'."
+ }
+}
+
+variable "scan_identifier" {
+ type = string
+ description = "Identifier of the scan"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", var.scan_identifier))
+ error_message = "scan_identifier must contain only alphanumeric characters, dashes, and underscores, and must not start or end with a dash or underscore."
+ }
+}
+
+variable "credentials_reference" {
+ type = string
+ description = "Name of the secret stored in Secrets Manager and containing the credentials to use for the scan"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", var.credentials_reference))
+ error_message = "The secret name is invalid"
+ }
+}
+
+variable "sns_topic_arn" {
+ type = string
+ default = null
+ description = "ARN of the SNS topic to use for notifications. Leave empty if SNS notifications are not needed."
+
+ validation {
+ condition = var.sns_topic_arn == null || can(regex("^arn:aws:sns:((af|ap|ca|eu|me|sa|us)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\\d+):[0-9]{12}:([a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*)$", var.sns_topic_arn))
+ error_message = "The SNS topic ARN is invalid"
+ }
+
+}
+
+variable "ami_owner" {
+ type = string
+ default = "amazon"
+ description = "Owner of the Amazon Machine Image (AMI) to use for the EC2 instance"
+}
+
+variable "ami_image_filter" {
+ type = string
+ default = "amzn2-ami-hvm*"
+ description = "Filter to use to find the Amazon Machine Image (AMI) to use for the EC2 instance the name can contain wildcards. Only GNU/Linux images are supported."
+
+}
+
+variable "instance_type" {
+ type = string
+ default = "t3a.medium"
+ description = "instance_type must be a valid AWS EC2 instance type."
+
+ validation {
+ condition = contains(jsondecode(file("../../../../../configuration/secrets-finder/aws/aws_ec2_instances.json")), var.instance_type)
+ error_message = "instance_type must be a valid AWS EC2 instance type."
+ }
+}
+
+variable "instance_user" {
+ type = string
+ default = "secrets-finder"
+ description = "Username to create and use on the instance started for the scanning process"
+
+ validation {
+ condition = can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", var.instance_user))
+ error_message = "instance_user must contain only alphanumeric characters, dashes, and underscores, and must not start or end with a dash or underscore."
+ }
+}
+
+variable "existing_security_groups" {
+ type = list(string)
+ default = []
+ description = "List of names representing existing security groups to add to the EC2 instance"
+}
+
+variable "new_security_groups" {
+ type = list(object({
+ name = string,
+ description = string,
+ ingress : optional(list(object({
+ from_port = number,
+ to_port = number,
+ protocol = any,
+ description = optional(string),
+ cidr_blocks = optional(list(string), []),
+ ipv6_cidr_blocks = optional(list(string), []),
+ security_groups = optional(list(string), []),
+ prefix_list_ids = optional(list(string), [])
+ })), []),
+ egress : optional(list(object({
+ from_port = number,
+ to_port = number,
+ protocol = any,
+ description = optional(string),
+ cidr_blocks = optional(list(string), []),
+ ipv6_cidr_blocks = optional(list(string), []),
+ security_groups = optional(list(string), []),
+ prefix_list_ids = optional(list(string), [])
+ })), [])
+ }))
+
+ default = []
+
+ description = "Security groups to create (see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group)"
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : (length(lookup(sg, "ingress", [])) != 0) || (length(lookup(sg, "egress", [])) != 0)])
+ error_message = "All security groups should contain at least one ingress or egress rule"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : (length(lookup(v, "cidr_blocks", [])) != 0) || (length(lookup(v, "ipv6_cidr_blocks", [])) != 0) || (length(lookup(v, "security_groups", [])) != 0) || (length(lookup(v, "prefix_list_ids", [])) != 0)])])
+ error_message = "All rules must define at least one of the following attributes: cidr_blocks, ipv6_cidr_blocks, security_groups, prefix_list_ids"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : can(regex("^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$", v["from_port"]))])])
+ error_message = "All 'from_port' values must refer to a valid port or a valid ICMP type number (see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule#from_port)"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : can(regex("^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$", v["to_port"]))])])
+ error_message = "All 'to_port' values must refer to a valid port or a valid ICMP type number (see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule#to_port)"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : can(regex("^(icmp(v6)?)|(tcp)|(udp)|(all)|((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([1-9][0-9])|([0-9]))$", v["protocol"]))])])
+ error_message = "All 'protocol' values must refer to a valid value (see: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule#protocol)"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : v["cidr_blocks"] == null || alltrue([for address in v["cidr_blocks"] : can(regex("^((25[0-5]|(2[0-4]|1[0-9]|[1-9])?[0-9]).){3}((25[0-5]|(2[0-4]|1[0-9]|[1-9])?[0-9]))/(0|0?[1-9]|[12][0-9]|3[012])$", address))])])])
+ error_message = "All 'cidr_blocks' should contain IP addresses denoted in CIDR format (xx.xx.xx.xx/yy)"
+ }
+
+ validation {
+ condition = alltrue([for sg in var.new_security_groups : alltrue([for v in concat(lookup(sg, "ingress", []), lookup(sg, "egress", [])) : v["ipv6_cidr_blocks"] == null || alltrue([for address in v["ipv6_cidr_blocks"] : can(regex("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/(([0-9])|([1-9][0-9])|(1[0-1][0-9])|(12[0-8]))$", address))])])])
+ error_message = "All 'ipv6_cidr_blocks' should contain IPv6 addresses denoted in CIDR format"
+ }
+}
+
+variable "trufflehog_version" {
+ type = string
+ default = "3.78.2"
+ description = "Version of TruffleHog to use"
+}
+
+variable "trufflehog_processes" {
+ type = number
+ default = 20
+ description = "Define the number of scanning processes that should be spawned by TruffleHog. WARNING: This may be resource intensive and consume all the host resources."
+
+ validation {
+ condition = (var.trufflehog_processes >= 1) && (var.trufflehog_processes <= 30)
+ error_message = "The number of scanning processes should be between 1 and 30 (included)"
+ }
+}
+
+variable "datadog_api_key_reference" {
+ type = string
+ default = null
+ description = "Name of the secret stored in Secrets Manager and containing the Datadog API key"
+
+ validation {
+ condition = (var.datadog_api_key_reference == null) || can(regex("^[a-zA-Z0-9]([a-zA-Z0-9-_]+[a-zA-Z0-9])*$", var.datadog_api_key_reference))
+ error_message = "The secret name is invalid"
+ }
+}
+
+variable "datadog_enable_ec2_instance_metrics" {
+ type = bool
+ default = true
+ description = "Enable the metrics for the EC2 instance in Datadog (should be 'true' if monitors are being used to track the health of the EC2 instance)"
+}
+
+variable "datadog_account" {
+ type = string
+ default = null
+ description = "The name of the Datadog account to which EC2 instance metrics should be reported and where monitors are set up. This variable is only used if 'datadog_enable_ec2_instance_metrics' variable is set to 'true'."
+}
diff --git a/infrastructure/secrets-finder/scheduled-scans/aws/scan/vpc.tf b/infrastructure/secrets-finder/scheduled-scans/aws/scan/vpc.tf
new file mode 100644
index 0000000..9103bd4
--- /dev/null
+++ b/infrastructure/secrets-finder/scheduled-scans/aws/scan/vpc.tf
@@ -0,0 +1,18 @@
+data "aws_vpc" "vpc" {
+ filter {
+ name = "tag:Name"
+ values = [var.vpc_name]
+ }
+}
+
+data "aws_subnets" "selected" {
+ filter {
+ name = "tag:Name"
+ values = [var.subnet_name]
+ }
+
+ filter {
+ name = "available-ip-address-count"
+ values = range(1, 200)
+ }
+}
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/README.md b/infrastructure/secrets-finder/setup/aws/secrets/README.md
new file mode 100644
index 0000000..acb24f8
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/README.md
@@ -0,0 +1,32 @@
+# secrets
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >=1.7 |
+| [aws](#requirement\_aws) | ~> 5.0 |
+
+## Providers
+
+No providers.
+
+## Modules
+
+No modules.
+
+## Resources
+
+No resources.
+
+## Inputs
+
+No inputs.
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [secrets\_finder\_secrets](#output\_secrets\_finder\_secrets) | ARNs of the secrets stored for use within secrets-finder |
+
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/helper.py b/infrastructure/secrets-finder/setup/aws/secrets/helper.py
new file mode 100644
index 0000000..c2087e6
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/helper.py
@@ -0,0 +1,134 @@
+import argparse
+import os
+import shlex
+import subprocess
+import sys
+
+
+def run_command(
+ command,
+ accepted_nonzero_return_codes=None,
+ env=None,
+ output_file=None,
+ error_file=None,
+ append_output=False,
+ append_error=False,
+):
+ if env is None:
+ env = os.environ.copy()
+ else:
+ env = {**os.environ.copy(), **env}
+
+ args = shlex.split(command)
+
+ output_mode = "a" if append_output else "w"
+ error_mode = "a" if append_error else "w"
+
+ out = open(output_file, output_mode) if output_file else None
+ err = open(error_file, error_mode) if error_file else None
+
+ process = subprocess.Popen(args, stdout=out, stderr=err, env=env)
+ stdout, stderr = process.communicate()
+
+ if output_file:
+ out.close()
+ if error_file:
+ err.close()
+
+ if process.returncode != 0 and (
+ accepted_nonzero_return_codes is None
+ or process.returncode not in accepted_nonzero_return_codes
+ ):
+ error_message = f"Command '{command}' failed"
+ if stderr:
+ error_message += f" with error: {stderr.decode()}"
+ raise Exception(error_message)
+
+ return stdout.decode() if stdout else None, stderr.decode() if stderr else None
+
+
+def configure_parser():
+ parser = argparse.ArgumentParser(
+ prog="secrets-finder-helper",
+ description="This script offers a wrapper to create secrets in Secrets Manager using a file encrypted with SOPS.",
+ epilog="This script has been developed by Thomson Reuters. For issues, comments or help, you can contact the maintainers on the official GitHub repository: https://github.com/thomsonreuters/secrets-finder",
+ )
+
+ parser.add_argument(
+ "--preserve-decrypted-file",
+ help="whether to preserve the decrypted file at the end of execution",
+ action="store_true",
+ default=os.environ.get(
+ "SECRETS_FINDER_PRESERVE_DECRYPTED_FILE", "false"
+ ).lower()
+ == "true",
+ )
+ parser.add_argument(
+ "--ignore-warning",
+ help="whether to ignore warning",
+ action="store_true",
+ default=os.environ.get("SECRETS_FINDER_IGNORE_WARNING", "false").lower()
+ == "true",
+ )
+ parser.add_argument(
+ "--sops-binary-path",
+ help="the path to the SOPS binary",
+ default=os.environ.get("SECRETS_FINDER_SOPS_BINARY_PATH", "sops"),
+ )
+ parser.add_argument(
+ "--terraform-command",
+ help="terraform command to run ('plan', 'apply', 'destroy')",
+ required=True,
+ choices=["plan", "apply", "destroy"],
+ )
+ parser.add_argument(
+ "--terraform-options",
+ help="additional options to pass to the terraform command",
+ default="",
+ )
+ parser.add_argument(
+ "--aws-profile",
+ help="AWS profile to use",
+ default=os.environ.get("AWS_PROFILE", "default"),
+ )
+
+ return parser.parse_args()
+
+
+def main():
+ try:
+ arguments = configure_parser()
+ except Exception:
+ sys.exit(1)
+
+ try:
+ if arguments.preserve_decrypted_file and not arguments.ignore_warning:
+ print(
+ "WARNING: The decrypted file will be preserved at the end of the execution. Make sure to remove it manually."
+ )
+ confirmation = input("Type 'yes' to continue, or any other key to abort: ")
+ if confirmation.lower() != "yes" and confirmation.lower() != "y":
+ print("Operation aborted.")
+ sys.exit()
+
+ run_command(
+ f"{arguments.sops_binary_path} -d secrets.enc.json --aws-profile {arguments.aws_profile}",
+ output_file="secrets.json",
+ )
+
+ try:
+ terraform_command = (
+ f"terraform {arguments.terraform_command} {arguments.terraform_options}"
+ )
+ return run_command(terraform_command)
+ except:
+ pass
+ except Exception as e:
+ print(f"ERROR: {e}")
+ finally:
+ if not arguments.preserve_decrypted_file:
+ os.remove("secrets.json")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/locals.tf b/infrastructure/secrets-finder/setup/aws/secrets/locals.tf
new file mode 100644
index 0000000..9f74f98
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/locals.tf
@@ -0,0 +1,6 @@
+locals {
+ environment = replace(lower(var.environment_type), " ", "-")
+ tags = merge(try(var.tags, {}), { environment = local.environment })
+
+ secrets = jsondecode(file("secrets.json"))
+}
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/outputs.tf b/infrastructure/secrets-finder/setup/aws/secrets/outputs.tf
new file mode 100644
index 0000000..0618371
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/outputs.tf
@@ -0,0 +1,4 @@
+output "secrets_finder_secrets" {
+ value = { for s in aws_secretsmanager_secret.secrets_finder_secrets : s.name => s.arn }
+ description = "ARNs of the secrets stored for use within secrets-finder"
+}
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/providers.tf b/infrastructure/secrets-finder/setup/aws/secrets/providers.tf
new file mode 100644
index 0000000..d089c28
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/providers.tf
@@ -0,0 +1,20 @@
+terraform {
+ required_version = ">=1.7"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ }
+
+ backend "s3" {
+ encrypt = true
+ }
+}
+
+provider "aws" {
+ region = var.aws_region
+ profile = var.aws_profile
+ default_tags { tags = local.tags }
+}
diff --git a/infrastructure/secrets-finder/setup/aws/secrets/s3.tfbackend b/infrastructure/secrets-finder/setup/aws/secrets/s3.tfbackend
new file mode 100644
index 0000000..6fa4016
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/secrets/s3.tfbackend
@@ -0,0 +1,5 @@
+bucket = ""
+key = ""
+region = ""
+dynamodb_table = ""
+profile = ""
diff --git a/infrastructure/secrets-finder/setup/aws/storage/README.md b/infrastructure/secrets-finder/setup/aws/storage/README.md
new file mode 100644
index 0000000..b5f10de
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/README.md
@@ -0,0 +1,66 @@
+# storage
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >=1.7 |
+| [aws](#requirement\_aws) | ~> 5.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | ~> 5.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_iam_policy.s3_access_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+| [aws_iam_policy.s3_push_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+| [aws_iam_role.s3_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role.s3_push](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy_attachment.allow_access_to_s3_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_iam_role_policy_attachment.allow_push_to_s3_bucket](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_s3_bucket.secrets_finder](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
+| [aws_s3_bucket_lifecycle_configuration.versioning-bucket-config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource |
+| [aws_s3_bucket_public_access_block.disable_public_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
+| [aws_s3_bucket_versioning.versioning](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
+| [aws_iam_policy_document.s3_access_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.s3_access_policy_document](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.s3_push_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.s3_push_policy_document](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [aws\_profile](#input\_aws\_profile) | AWS profile to use for authentication | `string` | `"default"` | no |
+| [aws\_region](#input\_aws\_region) | AWS region where to deploy resources | `string` | `"us-east-1"` | no |
+| [create\_access\_role](#input\_create\_access\_role) | Whether to create an IAM role for accessing the S3 bucket | `bool` | `true` | no |
+| [create\_push\_role](#input\_create\_push\_role) | Whether to create an IAM role for accessing the S3 bucket | `bool` | `true` | no |
+| [days\_after\_permanent\_deletion\_of\_noncurrent\_versions](#input\_days\_after\_permanent\_deletion\_of\_noncurrent\_versions) | Number of days after permanent deletion of noncurrent versions | `number` | `90` | no |
+| [environment\_type](#input\_environment\_type) | Environment (PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB) | `string` | `"PRODUCTION"` | no |
+| [force\_destroy](#input\_force\_destroy) | A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. WARNING: Setting this to true will permanently delete all objects in the bucket when Terraform needs to destroy the resource. | `bool` | `false` | no |
+| [iam\_role\_path](#input\_iam\_role\_path) | The path to use when creating IAM roles | `string` | `"/"` | no |
+| [permissions\_boundary\_arn](#input\_permissions\_boundary\_arn) | The name of the IAM permissions boundary to attach to the IAM role created by the module (if 'create\_access\_role' is set to true) | `string` | `null` | no |
+| [principals\_authorized\_to\_access\_bucket](#input\_principals\_authorized\_to\_access\_bucket) | List of AWS account IDs or ARNs that are authorized to assume the role created by the module | `list(string)` | n/a | yes |
+| [principals\_authorized\_to\_push\_to\_bucket](#input\_principals\_authorized\_to\_push\_to\_bucket) | List of AWS account IDs or ARNs that are authorized to assume the role created by the module | `list(string)` | n/a | yes |
+| [project\_name](#input\_project\_name) | Name of the project (should be the same across all modules of secrets-finder to ensure consistency) | `string` | `"secrets-finder"` | no |
+| [s3\_bucket\_name](#input\_s3\_bucket\_name) | S3 bucket name where to upload the scripts | `string` | n/a | yes |
+| [tags](#input\_tags) | A map of tags to add to the resources | `map(string)` | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [s3\_access\_role](#output\_s3\_access\_role) | n/a |
+| [s3\_bucket](#output\_s3\_bucket) | ARN of the S3 bucket used for secrets-finder |
+| [s3\_push\_role](#output\_s3\_push\_role) | n/a |
+
diff --git a/infrastructure/secrets-finder/setup/aws/storage/iam.tf b/infrastructure/secrets-finder/setup/aws/storage/iam.tf
new file mode 100644
index 0000000..1cb192a
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/iam.tf
@@ -0,0 +1,102 @@
+data "aws_iam_policy_document" "s3_access_assume_role" {
+ count = var.create_access_role ? 1 : 0
+ statement {
+ effect = "Allow"
+ principals {
+ identifiers = var.principals_authorized_to_access_bucket
+ type = "AWS"
+ }
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+data "aws_iam_policy_document" "s3_access_policy_document" {
+ count = var.create_access_role ? 1 : 0
+ statement {
+ sid = "ListS3Bucket"
+ effect = "Allow"
+ actions = ["s3:ListBucket"]
+ resources = [aws_s3_bucket.secrets_finder.arn]
+ }
+
+ statement {
+ sid = "GetAndListObjectsInS3Bucket"
+ effect = "Allow"
+ actions = [
+ "s3:GetObject*",
+ "s3:ListObject*"
+ ]
+ resources = ["${aws_s3_bucket.secrets_finder.arn}/*"]
+ }
+}
+
+resource "aws_iam_policy" "s3_access_policy" {
+ count = var.create_access_role ? 1 : 0
+ name = "${var.project_name}-s3-access"
+ description = "Policy allowing to access the S3 bucket of secrets-finder"
+ policy = data.aws_iam_policy_document.s3_access_policy_document[0].json
+}
+
+resource "aws_iam_role_policy_attachment" "allow_access_to_s3_bucket" {
+ count = var.create_access_role ? 1 : 0
+
+ policy_arn = aws_iam_policy.s3_access_policy[0].arn
+ role = aws_iam_role.s3_access[0].name
+}
+
+resource "aws_iam_role" "s3_access" {
+ count = var.create_access_role ? 1 : 0
+ name = "${var.project_name}-s3-access"
+ assume_role_policy = data.aws_iam_policy_document.s3_access_assume_role[0].json
+ path = var.iam_role_path
+ permissions_boundary = var.permissions_boundary_arn
+}
+
+
+data "aws_iam_policy_document" "s3_push_assume_role" {
+ count = var.create_push_role ? 1 : 0
+ statement {
+ effect = "Allow"
+ principals {
+ identifiers = var.principals_authorized_to_push_to_bucket
+ type = "AWS"
+ }
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+
+data "aws_iam_policy_document" "s3_push_policy_document" {
+ count = var.create_push_role ? 1 : 0
+
+ statement {
+ sid = "GetAndListObjectsInS3Bucket"
+ effect = "Allow"
+ actions = [
+ "s3:PutObject*"
+ ]
+ resources = ["${aws_s3_bucket.secrets_finder.arn}/*"]
+ }
+}
+
+resource "aws_iam_policy" "s3_push_policy" {
+ count = var.create_push_role ? 1 : 0
+ name = "${var.project_name}-s3-push"
+ description = "Policy allowing to push objects in the S3 bucket of secrets-finder"
+ policy = data.aws_iam_policy_document.s3_push_policy_document[0].json
+}
+
+resource "aws_iam_role_policy_attachment" "allow_push_to_s3_bucket" {
+ count = var.create_push_role ? 1 : 0
+
+ policy_arn = aws_iam_policy.s3_push_policy[0].arn
+ role = aws_iam_role.s3_push[0].name
+}
+
+resource "aws_iam_role" "s3_push" {
+ count = var.create_push_role ? 1 : 0
+ name = "${var.project_name}-s3-push"
+ assume_role_policy = data.aws_iam_policy_document.s3_push_assume_role[0].json
+ path = var.iam_role_path
+ permissions_boundary = var.permissions_boundary_arn
+}
diff --git a/infrastructure/secrets-finder/setup/aws/storage/locals.tf b/infrastructure/secrets-finder/setup/aws/storage/locals.tf
new file mode 100644
index 0000000..34652b7
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/locals.tf
@@ -0,0 +1,4 @@
+locals {
+ environment = replace(lower(var.environment_type), " ", "-")
+ tags = merge(try(var.tags, {}), { environment = local.environment })
+}
diff --git a/infrastructure/secrets-finder/setup/aws/storage/outputs.tf b/infrastructure/secrets-finder/setup/aws/storage/outputs.tf
new file mode 100644
index 0000000..45790ad
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/outputs.tf
@@ -0,0 +1,14 @@
+output "s3_bucket" {
+ value = aws_s3_bucket.secrets_finder.arn
+ description = "ARN of the S3 bucket used for secrets-finder"
+}
+
+output "s3_access_role" {
+ value = var.create_access_role == true ? aws_iam_role.s3_access[*].arn : null
+ depends_on = [aws_iam_role.s3_access]
+}
+
+output "s3_push_role" {
+ value = var.create_push_role == true ? aws_iam_role.s3_push[*].arn : null
+ depends_on = [aws_iam_role.s3_push]
+}
diff --git a/infrastructure/secrets-finder/setup/aws/storage/providers.tf b/infrastructure/secrets-finder/setup/aws/storage/providers.tf
new file mode 100644
index 0000000..d089c28
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/providers.tf
@@ -0,0 +1,20 @@
+terraform {
+ required_version = ">=1.7"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ }
+
+ backend "s3" {
+ encrypt = true
+ }
+}
+
+provider "aws" {
+ region = var.aws_region
+ profile = var.aws_profile
+ default_tags { tags = local.tags }
+}
diff --git a/infrastructure/secrets-finder/setup/aws/storage/s3.tf b/infrastructure/secrets-finder/setup/aws/storage/s3.tf
new file mode 100644
index 0000000..125572d
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/s3.tf
@@ -0,0 +1,35 @@
+resource "aws_s3_bucket" "secrets_finder" {
+ bucket = var.s3_bucket_name
+ force_destroy = var.force_destroy != null ? var.force_destroy : false
+}
+
+resource "aws_s3_bucket_public_access_block" "disable_public_access" {
+ bucket = aws_s3_bucket.secrets_finder.id
+ block_public_acls = true
+ block_public_policy = true
+ restrict_public_buckets = true
+ ignore_public_acls = true
+}
+
+resource "aws_s3_bucket_versioning" "versioning" {
+ bucket = aws_s3_bucket.secrets_finder.id
+ versioning_configuration {
+ status = "Enabled"
+ }
+}
+
+resource "aws_s3_bucket_lifecycle_configuration" "versioning-bucket-config" {
+ depends_on = [aws_s3_bucket_versioning.versioning]
+
+ bucket = aws_s3_bucket.secrets_finder.id
+
+ rule {
+ id = "delete-non-current-versions"
+
+ noncurrent_version_expiration {
+ noncurrent_days = var.days_after_permanent_deletion_of_noncurrent_versions
+ }
+
+ status = "Enabled"
+ }
+}
diff --git a/infrastructure/secrets-finder/setup/aws/storage/s3.tfbackend b/infrastructure/secrets-finder/setup/aws/storage/s3.tfbackend
new file mode 100644
index 0000000..6fa4016
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/s3.tfbackend
@@ -0,0 +1,5 @@
+bucket = ""
+key = ""
+region = ""
+dynamodb_table = ""
+profile = ""
diff --git a/infrastructure/secrets-finder/setup/aws/storage/variables.tf b/infrastructure/secrets-finder/setup/aws/storage/variables.tf
new file mode 100644
index 0000000..6c14998
--- /dev/null
+++ b/infrastructure/secrets-finder/setup/aws/storage/variables.tf
@@ -0,0 +1,130 @@
+variable "aws_region" {
+ type = string
+ default = "us-east-1"
+ description = "AWS region where to deploy resources"
+
+ validation {
+ condition = can(regex("^(af|ap|ca|eu|me|sa|us)-(central|north|(north(?:east|west))|south|south(?:east|west)|east|west)-\\d+$", var.aws_region))
+ error_message = "You should enter a valid AWS region (https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html)"
+ }
+}
+
+variable "aws_profile" {
+ type = string
+ default = "default"
+ description = "AWS profile to use for authentication"
+}
+
+variable "environment_type" {
+ type = string
+ default = "PRODUCTION"
+ description = "Environment (PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB)"
+
+ validation {
+ condition = contains(["PRODUCTION", "PRE-PRODUCTION", "QUALITY ASSURANCE", "INTEGRATION TESTING", "DEVELOPMENT", "LAB"], var.environment_type)
+ error_message = "The environment type should be one of the following values: PRODUCTION, PRE-PRODUCTION, QUALITY ASSURANCE, INTEGRATION TESTING, DEVELOPMENT, LAB (case sensitive)"
+ }
+}
+
+variable "tags" {
+ type = map(string)
+ description = "A map of tags to add to the resources"
+ default = {}
+
+ validation {
+ condition = alltrue([for v in values(var.tags) : v != ""])
+ error_message = "Tag values must not be empty."
+ }
+}
+
+variable "permissions_boundary_arn" {
+ type = string
+ default = null
+ description = "The name of the IAM permissions boundary to attach to the IAM role created by the module (if 'create_access_role' is set to true)"
+
+ validation {
+ condition = can(regex("^arn:aws:iam::[0-9]{12}:policy\\/([a-zA-Z0-9-_.]+)$", var.permissions_boundary_arn))
+ error_message = "The provided ARN is not a valid ARN for a policy"
+ }
+}
+
+variable "iam_role_path" {
+ type = string
+ default = "/"
+ description = "The path to use when creating IAM roles"
+
+ validation {
+ condition = can(regex("^\\/([a-zA-Z0-9]+([-a-zA-Z0-9]*[a-zA-Z0-9]+)?\\/)*$", var.iam_role_path))
+ error_message = "The provided path is invalid"
+ }
+}
+
+variable "project_name" {
+ type = string
+ default = "secrets-finder"
+ description = "Name of the project (should be the same across all modules of secrets-finder to ensure consistency)"
+}
+
+variable "create_access_role" {
+ type = bool
+ default = true
+ description = "Whether to create an IAM role for accessing the S3 bucket"
+}
+
+variable "principals_authorized_to_access_bucket" {
+ type = list(string)
+ description = "List of AWS account IDs or ARNs that are authorized to assume the role created by the module"
+
+ validation {
+ condition = alltrue([for v in var.principals_authorized_to_access_bucket : can(regex("^(\\d{12}|(arn:aws:iam::(\\d{12})?:(role|user)((\\/)|(\\/[\\w+=,.@-]{1,128}\\/))[\\w+=,.@-]{1,128}))$", v))])
+ error_message = "One or more provided values are not a valid AWS account ID or ARN"
+ }
+
+ validation {
+ condition = length(var.principals_authorized_to_access_bucket) > 0
+ error_message = "At least one principal must be specified."
+ }
+}
+
+variable "create_push_role" {
+ type = bool
+ default = true
+ description = "Whether to create an IAM role for accessing the S3 bucket"
+}
+
+variable "principals_authorized_to_push_to_bucket" {
+ type = list(string)
+ description = "List of AWS account IDs or ARNs that are authorized to assume the role created by the module"
+
+ validation {
+ condition = alltrue([for v in var.principals_authorized_to_push_to_bucket : can(regex("^(\\d{12}|(arn:aws:iam::(\\d{12})?:(role|user)((\\/)|(\\/[\\w+=,.@-]{1,128}\\/))[\\w+=,.@-]{1,128}))$", v))])
+ error_message = "One or more provided values are not a valid AWS account ID or ARN"
+ }
+
+ validation {
+ condition = length(var.principals_authorized_to_push_to_bucket) > 0
+ error_message = "At least one principal must be specified."
+ }
+}
+
+variable "s3_bucket_name" {
+ type = string
+ description = "S3 bucket name where to upload the scripts"
+}
+
+variable "force_destroy" {
+ type = bool
+ default = false
+ description = "A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error. WARNING: Setting this to true will permanently delete all objects in the bucket when Terraform needs to destroy the resource."
+}
+
+variable "days_after_permanent_deletion_of_noncurrent_versions" {
+ type = number
+ default = 90
+ description = "Number of days after permanent deletion of noncurrent versions"
+
+ validation {
+ condition = var.days_after_permanent_deletion_of_noncurrent_versions >= 1
+ error_message = "The number of days after permanent deletion of noncurrent versions should be greater than or equal to 1"
+ }
+}
diff --git a/scripts/processors/github/api_github_organization.py b/scripts/processors/github/api_github_organization.py
new file mode 100644
index 0000000..af5eedc
--- /dev/null
+++ b/scripts/processors/github/api_github_organization.py
@@ -0,0 +1,378 @@
+import argparse
+import datetime
+import dotenv
+import glob
+import json
+import logging
+import logging.config
+import os
+import requests
+import requests.exceptions
+import sys
+import urllib.parse
+from time import sleep, time
+
+
+LOG_FUNCTIONS = {
+ "INFO": logging.info,
+ "WARNING": logging.warning,
+ "ERROR": logging.error,
+ "DEBUG": logging.debug,
+}
+
+
+def load_environment_variables(folder=os.getenv("SECRETS_FINDER_SCAN_FOLDER")):
+ if not folder:
+ dotenv.load_dotenv(override=True)
+ else:
+ dotenv_files = glob.glob(os.path.join(folder, "*.env"))
+ for file in dotenv_files:
+ if os.path.isfile(file):
+ dotenv.load_dotenv(dotenv_path=file, override=True)
+
+
+def positive_int(value):
+ ivalue = int(value)
+ if ivalue <= 0:
+ raise argparse.ArgumentTypeError(f"{value} is an invalid positive int value")
+ return ivalue
+
+
+def non_empty_string(value):
+ svalue = str(value)
+ if svalue == "":
+ raise argparse.ArgumentTypeError("value cannot be an empty string")
+ return svalue
+
+
+# This validation is used to reject common malformed URLs. It does not aim to strictly validate URLs.
+# More information: https://docs.python.org/3/library/urllib.parse.html#url-parsing-security
+def valid_uri(value):
+ try:
+ result = urllib.parse.urlparse(value)
+ return value if all([result.scheme, result.netloc]) else None
+ except ValueError:
+ pass
+ raise argparse.ArgumentTypeError(f"Invalid URI: {value}")
+
+
+def configure_parser():
+ parser = argparse.ArgumentParser(
+ prog="github-organization-processor",
+ description="This script fetches all the repositories of a GitHub organization using the standard GitHub API. This script supports both GitHub Enterprise Cloud and GitHub Enterprise Server.",
+ epilog="This script has been developed by Thomson Reuters. For issues, comments or help, you can contact the maintainers on the official GitHub repository: https://github.com/thomsonreuters/secrets-finder",
+ )
+
+ parser.add_argument(
+ "--debug",
+ action="store_true",
+ help="show debug information",
+ default=os.environ.get("GITHUB_ORGANIZATION_PROCESSOR_DEBUG", False),
+ )
+ parser.add_argument(
+ "--api",
+ help="base URL of the API",
+ type=valid_uri,
+ default=os.environ.get(
+ "GITHUB_ORGANIZATION_PROCESSOR_API", "https://api.github.com"
+ ),
+ )
+ parser.add_argument(
+ "--clone-url-template",
+ help="template for the clone URL",
+ type=non_empty_string,
+ default=os.environ.get(
+ "GITHUB_ORGANIZATION_PROCESSOR_CLONE_URL_TEMPLATE",
+ "https://github.com/{organization}/{repository}",
+ ),
+ )
+ parser.add_argument(
+ "--organization",
+ help="GitHub organization for which repositories should be fetched",
+ type=non_empty_string,
+ required=os.environ.get("GITHUB_ORGANIZATION_PROCESSOR_ORGANIZATION") is None,
+ default=os.environ.get("GITHUB_ORGANIZATION_PROCESSOR_ORGANIZATION"),
+ )
+ parser.add_argument(
+ "--max-retries",
+ help="maximum number of retries for rate limiting",
+ type=positive_int,
+ default=os.environ.get("GITHUB_ORGANIZATION_PROCESSOR_MAX_RETRIES", 10),
+ )
+ parser.add_argument(
+ "--backoff-factor",
+ help="backoff factor for rate limiting",
+ type=positive_int,
+ default=os.environ.get("GITHUB_ORGANIZATION_PROCESSOR_BACKOFF_FACTOR", 1),
+ )
+
+ return parser
+
+
+def configure_logging(destination_folder, level=logging.INFO):
+ log_file = "github-organization-processor.log"
+ logging.config.dictConfig({"version": 1, "disable_existing_loggers": True})
+ logging.basicConfig(
+ format="%(message)s", filename=f"{destination_folder}/{log_file}", level=level
+ )
+
+
+def log(level, context, message):
+ current_time = str(datetime.datetime.now())
+
+ log_string = json.dumps(
+ {"time": current_time, "level": level, "context": context, "message": message},
+ separators=(",", ":"),
+ )
+
+ return LOG_FUNCTIONS[level]("%s", log_string)
+
+
+class MaxRetriesExceededError(Exception):
+ """Exception raised when the maximum number of retries is exceeded."""
+
+ pass
+
+
+class GitHubClient:
+ def __init__(self, api, token, max_retries=10, backoff_factor=1):
+ log("INFO", "GITHUB-ORGANIZATION-PROCESSOR", "Configuring GitHub client...")
+
+ self.api = api
+ self.token = token
+ self.headers = {
+ "Authorization": f"token {token}",
+ "Accept": "Accept: application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+ }
+ self.max_retries = max_retries
+ self.backoff_factor = backoff_factor
+
+ log(
+ "INFO",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"GitHub client configured successfully for API: {api}",
+ )
+ log(
+ "DEBUG",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"GitHub client configured with token starting with: {token[:8]}",
+ )
+
+ def make_api_request(self, method, url, **kwargs):
+ valid_methods = ["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]
+ if method not in valid_methods:
+ raise ValueError(
+ f"Invalid HTTP method: {method}. Must be one of {valid_methods}."
+ )
+
+ max_retries = self.max_retries
+ backoff_factor = self.backoff_factor
+ rate_limit_retry_count = 0
+
+ while True:
+ try:
+ log("DEBUG", "GITHUB-CLIENT", f"{method} request to {url}")
+ response = requests.request(method, url, headers=self.headers, **kwargs)
+
+ if 200 <= response.status_code < 300:
+ log(
+ "DEBUG",
+ "GITHUB-CLIENT",
+ f"Status code returned by {url}: {response.status_code}",
+ )
+ log(
+ "DEBUG",
+ "GITHUB-CLIENT",
+ f"Response returned by {url}: {response.json()}",
+ )
+ return response
+ elif (
+ response.status_code == 403
+ and "X-RateLimit-Reset" in response.headers
+ ):
+ if rate_limit_retry_count >= max_retries:
+ raise MaxRetriesExceededError(
+ f"Rate limit retry count exceeded for {url}"
+ )
+
+ reset_timestamp = int(response.headers["X-RateLimit-Reset"])
+ current_timestamp = int(time())
+
+ if reset_timestamp <= current_timestamp:
+ continue
+
+ sleep_time = reset_timestamp - current_timestamp + 1
+ log(
+ "WARNING",
+ "GITHUB-CLIENT",
+ f"Rate limit hit. Sleeping for {sleep_time} seconds.",
+ )
+ sleep(sleep_time)
+ rate_limit_retry_count += 1
+ elif response.status_code == 429:
+ if rate_limit_retry_count >= max_retries:
+ raise MaxRetriesExceededError(
+ f"Rate limit retry count exceeded for {url}"
+ )
+
+ sleep_time = backoff_factor
+ log(
+ "WARNING",
+ "GITHUB-CLIENT",
+ f"Too many requests. Sleeping for {sleep_time} seconds.",
+ )
+ sleep(sleep_time)
+ backoff_factor *= 2
+ rate_limit_retry_count += 1
+ else:
+ response.raise_for_status()
+
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
+ log(
+ "WARNING",
+ "GITHUB-CLIENT",
+ "Request timed out or connection error occurred. Waiting to retry...",
+ )
+ sleep(10)
+ except requests.exceptions.RequestException as e:
+ log(
+ "DEBUG",
+ "GITHUB-CLIENT",
+ f"An error occurred while executing a {method} request to {url}: {e}",
+ custom_color="red",
+ )
+ raise e
+
+ def get_repositories(self, organization):
+ url = f"{self.api}/orgs/{organization}/repos"
+ repositories = []
+ while url:
+ response = self.make_api_request(method="GET", url=url)
+ repositories.extend(response.json())
+ url = response.links.get("next", {}).get("url")
+
+ log(
+ "DEBUG",
+ "GITHUB-CLIENT",
+ f"Number of repositories found for organization {organization}: {len(repositories)}",
+ )
+ return repositories
+
+
+def persist_repositories_information(
+ organization,
+ repositories,
+ location=os.environ.get("SECRETS_FINDER_SCAN_FOLDER", "."),
+ filename="repositories.json",
+):
+ log(
+ "INFO",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"Persisting list of repositories for organization {organization} to: {location}/{filename}",
+ )
+
+ formatted_list_of_repositories = {
+ "organization": organization,
+ "repositories": repositories,
+ }
+
+ with open(f"{location}/{filename}", "w") as file:
+ json.dump(formatted_list_of_repositories, file, indent=4)
+
+ log(
+ "INFO",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"List of repositories for organization {organization} persisted successfully to: {location}/{filename}",
+ )
+
+
+def persist_repositories_for_scan(
+ organization,
+ repositories,
+ clone_url_template,
+ location=os.environ.get("SECRETS_FINDER_SCAN_FOLDER", "."),
+ filename="repositories_to_scan.json",
+):
+ log(
+ "INFO",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"Persisting list of repositories for organization {organization} to: {location}/{filename}",
+ )
+
+ formatted_list_of_repositories = {
+ "scm": "github",
+ "endpoint": clone_url_template,
+ "repositories": [],
+ }
+
+ for repository in repositories:
+ formatted_list_of_repositories.get("repositories").append(
+ {"organization": organization, "name": repository.get("name")}
+ )
+
+ with open(f"{location}/{filename}", "w") as file:
+ json.dump(formatted_list_of_repositories, file, indent=4)
+
+ log(
+ "INFO",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"List of repositories for organization {organization} persisted successfully to: {location}/{filename}",
+ )
+
+
+def main():
+ try:
+ load_environment_variables()
+ parser = configure_parser()
+ arguments = parser.parse_args()
+ configure_logging(".", logging.INFO if not arguments.debug else logging.DEBUG)
+ except Exception as exception:
+ print(
+ f"FATAL ERROR: An unexpected error occurred during initialization: {str(exception)}"
+ )
+ sys.exit(1)
+
+ try:
+ if not os.environ.get("SECRETS_FINDER_SCAN_TOKEN") and not os.environ.get(
+ "GITHUB_TOKEN"
+ ):
+ log(
+ "ERROR",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ "No token provided: SECRETS_FINDER_SCAN_TOKEN and GITHUB_TOKEN environment variables are both missing. Operation aborted.",
+ )
+ sys.exit(1)
+
+ github_client = GitHubClient(
+ api=arguments.api,
+ token=os.environ.get(
+ "SECRETS_FINDER_SCAN_TOKEN", os.environ.get("GITHUB_TOKEN")
+ ),
+ max_retries=arguments.max_retries,
+ backoff_factor=arguments.backoff_factor,
+ )
+ repositories = github_client.get_repositories(
+ organization=arguments.organization
+ )
+ persist_repositories_information(
+ organization=arguments.organization, repositories=repositories
+ )
+ persist_repositories_for_scan(
+ organization=arguments.organization,
+ repositories=repositories,
+ api=arguments.clone_url_template,
+ )
+
+ sys.exit(0)
+ except Exception as exception:
+ log(
+ "ERROR",
+ "GITHUB-ORGANIZATION-PROCESSOR",
+ f"A fatal error occurred during scan: {str(exception)}. Operation aborted.",
+ )
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/processors/github/api_github_organization.requirements.txt b/scripts/processors/github/api_github_organization.requirements.txt
new file mode 100644
index 0000000..2d7083d
--- /dev/null
+++ b/scripts/processors/github/api_github_organization.requirements.txt
@@ -0,0 +1,2 @@
+python-dotenv ~= 1.0.1
+requests ~= 2.32