├── assets ├── v2-walkthrough.png ├── component-overview.png └── document-screenshot.png ├── versions.tf ├── examples └── basic │ ├── terraform.tf │ ├── output.tf │ ├── main.tf │ └── README.md ├── .tflint.hcl ├── output.tf ├── .gitignore ├── lambda ├── package.json ├── logger.js ├── index.js └── package-lock.json ├── LICENSE ├── variables.tf ├── CONTRIBUTING.md ├── README.md └── main.tf /assets/v2-walkthrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iKnowJavaScript/terraform-aws-vulne-soldier/HEAD/assets/v2-walkthrough.png -------------------------------------------------------------------------------- /assets/component-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iKnowJavaScript/terraform-aws-vulne-soldier/HEAD/assets/component-overview.png -------------------------------------------------------------------------------- /assets/document-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iKnowJavaScript/terraform-aws-vulne-soldier/HEAD/assets/document-screenshot.png -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.0" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /examples/basic/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.0" 8 | } 9 | } 10 | } 11 | 12 | provider "aws" { 13 | region = "us-east-1" 14 | } 15 | -------------------------------------------------------------------------------- /.tflint.hcl: -------------------------------------------------------------------------------- 1 | config { 2 | format = "compact" 3 | module = true 4 | } 5 | 6 | plugin "aws" { 7 | enabled = true 8 | version = "0.30.0" 9 | source = "github.com/terraform-linters/tflint-ruleset-aws" 10 | } 11 | 12 | rule "terraform_comment_syntax" { 13 | enabled = true 14 | } 15 | 16 | rule "terraform_naming_convention" { 17 | enabled = true 18 | } 19 | 20 | rule "terraform_documented_variables" { 21 | enabled = true 22 | } -------------------------------------------------------------------------------- /examples/basic/output.tf: -------------------------------------------------------------------------------- 1 | output "lambda_function_arn" { 2 | description = "Lambda function ARN" 3 | value = module.remediation.lambda_function_arn 4 | } 5 | 6 | output "lambda_function_name" { 7 | description = "Lambda function name" 8 | value = module.remediation.lambda_function_name 9 | } 10 | 11 | output "ssm_document_name" { 12 | description = "SSM document name" 13 | value = module.remediation.ssm_document_name 14 | } 15 | 16 | -------------------------------------------------------------------------------- /output.tf: -------------------------------------------------------------------------------- 1 | 2 | output "lambda_function_arn" { 3 | description = "Lambda function ARN" 4 | value = aws_lambda_function.inspector_remediation.arn 5 | } 6 | 7 | output "lambda_function_name" { 8 | description = "Lambda function name" 9 | value = aws_lambda_function.inspector_remediation.function_name 10 | } 11 | 12 | output "ssm_document_name" { 13 | description = "SSM document name" 14 | value = aws_ssm_document.remediation_document.name 15 | } 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Terraform ### 2 | # Local .terraform directories 3 | **/.terraform/* 4 | 5 | # .tfstate files 6 | *.tfstate 7 | *.tfstate.* 8 | 9 | # Crash log files 10 | crash.log 11 | crash.*.log 12 | 13 | # Ignore override files as they are usually used to override resources locally and so 14 | # are not checked in 15 | override.tf 16 | override.tf.json 17 | *_override.tf 18 | *_override.tf.json 19 | 20 | # Ignore CLI configuration files 21 | .terraformrc 22 | terraform.rc 23 | 24 | node_modules/ 25 | lambda.zip 26 | *.zip -------------------------------------------------------------------------------- /lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manual-compliance-remediate-lambda-function", 3 | "version": "1.0.0", 4 | "description": "Lambda function that handles compliance findings and remediate it.", 5 | "main": "manual-compliance-remediate.js", 6 | "engines": { 7 | "node": "18", 8 | "npm": "9" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "node manual-compliance-remediate.js", 13 | "zip": "zip -r ../lambda.zip ." 14 | }, 15 | "keywords": [ 16 | "aws", 17 | "lambda", 18 | "compliance" 19 | ], 20 | "author": "Victor Omolayo", 21 | "license": "ISC", 22 | "dependencies": { 23 | "aws-sdk": "^2.1531.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lambda/logger.js: -------------------------------------------------------------------------------- 1 | // Logger 2 | const LogLevel = { 3 | none: 0, 4 | error: 10, 5 | info: 20, 6 | debug: 30, 7 | }; 8 | 9 | class Logger { 10 | constructor(logLevel) { 11 | this.logLevel = logLevel; 12 | } 13 | 14 | error(...args) { 15 | if (this.logLevel >= LogLevel.error) { 16 | console.error(...args); 17 | } 18 | } 19 | info(...args) { 20 | if (this.logLevel >= LogLevel.info) { 21 | console.log(...args); 22 | } 23 | } 24 | debug(...args) { 25 | if (this.logLevel >= LogLevel.debug) { 26 | console.trace(...args); 27 | } 28 | } 29 | } 30 | 31 | const logLevel = 32 | { 33 | ERROR: LogLevel.error, 34 | INFO: LogLevel.info, 35 | DEBUG: LogLevel.debug, 36 | }[process.env.LOG_LEVEL ?? "NONE"] ?? LogLevel.none; 37 | 38 | module.exports = new Logger(logLevel); -------------------------------------------------------------------------------- /examples/basic/main.tf: -------------------------------------------------------------------------------- 1 | module "remediation" { 2 | source = "iKnowJavaScript/vulne-soldier/aws" 3 | version = "2.0.0" 4 | 5 | name = "vulne-soldier-compliance-remediate" 6 | environment = "prod" 7 | aws_region = "us-east-1" 8 | account_id = "111122223333" 9 | lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" 10 | path_to_lambda_zip = "../../lambda.zip" 11 | remediation_options = [{ 12 | region = "us-east-1" 13 | reboot_option = "NoReboot" 14 | target_ec2_tag_name = "AmazonECSManaged" 15 | target_ec2_tag_value = "true" 16 | vulnerability_severities = "CRITICAL, HIGH" 17 | override_findings_for_target_instances_ids = "" 18 | }] 19 | remediation_schedule_days = ["15", "L"] 20 | ssn_notification_topic_arn = null 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Victor Omolayo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "Name of the application" 3 | type = string 4 | default = "vulne-soldier-compliance-remediate" 5 | } 6 | 7 | variable "aws_region" { 8 | description = "AWS region where the resources will be created" 9 | type = string 10 | default = "us-east-1" 11 | } 12 | 13 | variable "environment" { 14 | description = "Name of the environment" 15 | type = string 16 | default = "dev" 17 | } 18 | 19 | variable "account_id" { 20 | description = "AWS account ID" 21 | type = string 22 | validation { 23 | condition = can(regex("^[0-9]{12}$", var.account_id)) 24 | error_message = "The account_id must be a 12-digit number." 25 | } 26 | } 27 | 28 | variable "lambda_log_group" { 29 | description = "Name of the CloudWatch Log Group for the Lambda function" 30 | type = string 31 | } 32 | 33 | variable "path_to_lambda_zip" { 34 | description = "File location of the lambda zip file for remediation." 35 | type = string 36 | validation { 37 | condition = can(regex("^.+\\.zip$", var.path_to_lambda_zip)) 38 | error_message = "The path_to_lambda_zip must be a path to a zip file." 39 | } 40 | } 41 | 42 | variable "remediation_options" { 43 | description = "List of remediation option objects" 44 | type = list(object({ 45 | region = string 46 | reboot_option = string 47 | target_ec2_tag_name = string 48 | target_ec2_tag_value = string 49 | vulnerability_severities = string 50 | override_findings_for_target_instances_ids = string 51 | })) 52 | default = [ 53 | { 54 | region = "us-east-1" 55 | reboot_option = "NoReboot" 56 | target_ec2_tag_name = "AmazonECSManaged" 57 | target_ec2_tag_value = "true" 58 | vulnerability_severities = "CRITICAL, HIGH" 59 | override_findings_for_target_instances_ids = null 60 | } 61 | ] 62 | validation { 63 | condition = alltrue([ 64 | for opt in var.remediation_options : contains(["NoReboot", "RebootIfNeeded"], opt.reboot_option) 65 | ]) 66 | error_message = "Each remediation_option.reboot_option must be either NoReboot or RebootIfNeeded." 67 | } 68 | validation { 69 | condition = alltrue([ 70 | for opt in var.remediation_options : can(regex("^([A-Z]+, )*[A-Z]+$", opt.vulnerability_severities)) 71 | ]) 72 | error_message = "Each remediation_option.vulnerability_severities must be a comma-separated list of severities in uppercase." 73 | } 74 | } 75 | 76 | variable "ssn_notification_topic_arn" { 77 | description = "SNS topic ARN for notifications" 78 | type = string 79 | default = null 80 | validation { 81 | condition = var.ssn_notification_topic_arn == null || can(regex("^arn:aws:sns:[a-z0-9-]+:[0-9]{12}:[a-zA-Z0-9_-]+$", var.ssn_notification_topic_arn)) 82 | error_message = "The ssn_notification_topic_arn must be null or a valid SNS topic ARN." 83 | } 84 | } 85 | 86 | variable "remediation_schedule_days" { 87 | description = "List of days in the month to trigger remediation (e.g., [15, \"L\"] for 15th and last day)" 88 | type = list(string) 89 | default = ["15", "L"] 90 | validation { 91 | condition = length(var.remediation_schedule_days) > 0 && alltrue([for d in var.remediation_schedule_days : can(regex("^(0?[1-9]|[12][0-9]|3[01]|L)$", d))]) 92 | error_message = "Each value in remediation_schedule_days must be a day number (1-31) or 'L' for last day." 93 | } 94 | } -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Example: Basic Usage of vulne-soldier 2 | 3 | Deploy your AWS EC2 vulnerability remediation tool using this Terraform module. It provisions essential resources such as an SSM document, Lambda function, and CloudWatch event rules for automated vulnerability management. 4 | 5 | ## Overview 6 | 7 | This module sets up an automated vulnerability remediation environment optimized for production use. By creating an SSM document to define the remediation steps, setting up a Lambda function to execute the remediation, and establishing CloudWatch event rules to trigger the process based on AWS Inspector findings, the module offers a straightforward approach to managing EC2 vulnerabilities on AWS. 8 | 9 | ## How to Use This Module 10 | 11 | To deploy the `vulne-soldier` module, you can use the following configuration in your Terraform setup: 12 | 13 | ```hcl 14 | module "remediation" { 15 | source = "../../" 16 | 17 | name = "vulne-soldier-compliance-remediate" 18 | environment = "dev" 19 | aws_region = "us-east-1" 20 | account_id = "2324334432" 21 | lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" 22 | lambda_zip = "../../lambda.zip" 23 | remediation_options = { 24 | region = "us-east-1" 25 | reboot_option = "NoReboot" 26 | target_ec2_tag_name = "AmazonECSManaged" 27 | target_ec2_tag_value = "true" 28 | vulnerability_severities = ["CRITICAL, HIGH"] 29 | override_findings_for_target_instances_ids = [] 30 | } 31 | } 32 | 33 | provider "aws" { 34 | region = "us-east-1" 35 | } 36 | ``` 37 | 38 | ### Inputs 39 | 40 | | Name | Description | Type | Default | Required | 41 | |------------------------------------------|-----------------------------------------------------------------------------|---------------|--------------------------------------------|:--------:| 42 | | `name` | Name of the application | `string` | n/a | yes | 43 | | `environment` | Name of the environment | `string` | n/a | yes | 44 | | `aws_region` | AWS region where the resources will be created | `string` | n/a | yes | 45 | | `account_id` | AWS account ID | `string` | n/a | yes | 46 | | `lambda_log_group` | Name of the CloudWatch Log Group for the Lambda function | `string` | n/a | yes | 47 | | `lambda_zip` | File location of the lambda zip file for remediation | `string` | `lambda.zip` | yes | 48 | | `remediation_options` | Options for the remediation document | `object` | n/a | yes | 49 | | `remediation_options.region` | The region to use | `string` | `us-east-1` | no | 50 | | `remediation_options.reboot_option` | Reboot option for patching | `string` | `NoReboot` | no | 51 | | `remediation_options.target_ec2_tag_name`| The tag name to filter EC2 instances | `string` | `AmazonECSManaged` | no | 52 | | `remediation_options.target_ec2_tag_value`| The tag value to filter EC2 instances | `string` | `true` | no | 53 | | `remediation_options.vulnerability_severities`| Comma separated list of vulnerability severities to filter findings | `string`| `"CRITICAL, HIGH"` | no | 54 | | `remediation_options.override_findings_for_target_instances_ids`| Comma separated list of instance IDs to override findings for target instances | `string`| `""` | no | 55 | 56 | ### Outputs 57 | 58 | | Name | Description | Sensitive | 59 | |-----------------------|------------------------------|:---------:| 60 | | `lambda_function_arn` | Lambda function ARN | No | 61 | | `lambda_function_name`| Lambda function name | No | 62 | | `ssm_document_name` | SSM document name | No | 63 | 64 | To retrieve outputs, use the `terraform output` command, for example: `terraform output lambda_function_arn`. 65 | 66 | ## License 67 | 68 | This code is provided under the MIT License. Full licensing details are available in the included LICENSE.md file. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Forest Terraform 2 | 3 | We'd love for you to contribute to our source code and to make the Forest even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | * [Issues and Bugs](#issue) 6 | * [Feature Requests](#feature) 7 | * [Submission Guidelines](#submit) 8 | * [Further Info](#info) 9 | 10 | 11 | ## Found an Issue? 12 | 13 | If you find a bug in the source code or a mistake in the documentation, you can help us by submitting an issue to our [Github Repository][github]. Even better you can submit a Pull Request with a fix. 14 | 15 | **Please see the [Submission Guidelines](#submit) below.** 16 | 17 | ## Want a Feature? 18 | 19 | You can request a new feature by submitting an issue to our [Github Repository][github]. If you would like to implement a new feature then consider what kind of change it is: 20 | 21 | * **Major Changes** 22 | * **Small Changes** can be crafted and submitted to the [Github Repository][github] as a Pull Request. 23 | 24 | ## Want a Doc Fix? 25 | 26 | If you want to help improve the docs, it's a good idea to let others know what you're working on to minimize duplication of effort. Create a new issue (or comment on a related existing one) to let others know what you're working on. 27 | 28 | For large fixes, please build and test the documentation before submitting the MR to be sure you haven't accidentally introduced any layout or formatting issues. You should also make sure that your commit message starts with "docs" and follows the **[Commit Message Guidelines](#commit)** outlined below. 29 | 30 | ## Submission Guidelines 31 | 32 | ### Submitting an Issue 33 | 34 | Before you submit your issue search the archive, maybe your question was already answered. 35 | 36 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. Providing the following information will increase the chances of your issue being dealt with quickly: 37 | 38 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 39 | * **Motivation for or Use Case** - explain why this is a bug for you 40 | * **Forest Version(s)** - is it a regression? 41 | * **Reproduce the Error** - try to describe how to reproduce the error 42 | * **Related Issues** - has a similar issue been reported before? 43 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 44 | causing the problem (line of code or commit) 45 | 46 | **If you get help, help others. Good karma rulez!** 47 | 48 | ### Submitting a Pull Request 49 | 50 | Before you submit your pull request consider the following guidelines: 51 | 52 | * Make your changes in a new git branch: 53 | 54 | ```shell 55 | git checkout -b my-fix-branch main 56 | ``` 57 | 58 | * Create your patch, **including appropriate test cases**. 59 | * Install [Terraform](https://www.terraform.io/). check `required_version` in `versions.tf` for the current development version of the module. 60 | * Installs 61 | * Install [tflint](https://github.com/terraform-linters/tflint). We use tflint to lint the terraform code. 62 | * Initialize the terraform modules: 63 | 64 | ```shell 65 | terraform init 66 | ``` 67 | 68 | * For updating docs, you have to enable GitHub actions on your forked repository. Simply go to the tab Actions and enable actions. 69 | * Commit your changes using a descriptive commit message: 70 | 71 | ```shell 72 | git commit -a 73 | ``` 74 | 75 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 76 | 77 | * Push your branch to Github: 78 | 79 | ```shell 80 | git push origin my-fix-branch 81 | ``` 82 | 83 | In Github, send a pull request to original main branch: f.e. `terraform-aws-complete-static-site:main`. 84 | If we suggest changes, then: 85 | 86 | * Make the required updates. 87 | * Re-run the test suite to ensure tests are still passing. 88 | * Commit your changes to your branch (e.g. `my-fix-branch`). 89 | * Push the changes to your Github repository (this will update your Pull Request). 90 | 91 | If the PR gets too outdated we may ask you to rebase and force push to update the PR: 92 | 93 | ```shell 94 | git rebase main -i 95 | git push origin my-fix-branch -f 96 | ``` 97 | 98 | _WARNING: Squashing or reverting commits and force-pushing thereafter may remove Github comments on code that were previously made by you or others in your commits. Avoid any form of rebasing unless necessary. 99 | 100 | That's it! Thank you for your contribution! 101 | 102 | #### After your merge request is merged 103 | 104 | After your pull request is merged, you can safely delete your branch and pull the changes 105 | from the main (upstream) repository: 106 | 107 | * Delete the remote branch on Github either through the Github web UI or your local shell as follows: 108 | 109 | ```shell 110 | git push origin --delete my-fix-branch 111 | ``` 112 | 113 | * Check out the main branch: 114 | 115 | ```shell 116 | git checkout main -f 117 | ``` 118 | 119 | * Delete the local branch: 120 | 121 | ```shell 122 | git branch -D my-fix-branch 123 | ``` 124 | 125 | * Update your main with the latest upstream version: 126 | 127 | ```shell 128 | git pull --ff upstream main 129 | ``` 130 | 131 | ## Info 132 | 133 | [contribute]: CONTRIBUTING.md 134 | [github]: https://github.com/iKnowJavaScript/terraform-aws-complete-static-site/issues 135 | -------------------------------------------------------------------------------- /lambda/index.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | const logger = require('./logger'); 3 | 4 | exports.handler = async (event) => { 5 | const REGION = event.region || 'us-east-1'; 6 | const REBOOT_OPTION = event.reboot_option || "NoReboot"; 7 | const TARGET_EC2_TAG_NAME = event.target_ec2_tag_name; 8 | const TARGET_EC2_TAG_VALUE = event.target_ec2_tag_value; 9 | const VULNERABILITY_SEVERITIES = (event.vulnerability_severities 10 | && event.vulnerability_severities?.split(',')?.map(a => a.trim().toUpperCase())) || []; 11 | const OVERRIDE_FINDINGS_FOR_TARGET_INSTANCES_IDS = (event.override_findings_for_target_instances_ids 12 | && event.override_findings_for_target_instances_ids?.split(',')?.map(a => a.trim())) || []; 13 | 14 | logger.info("Region is ", REGION); 15 | logger.info("Reboot option is ", REBOOT_OPTION); 16 | logger.info("Target EC2 tag name is ", TARGET_EC2_TAG_NAME); 17 | logger.info("Target EC2 tag value is ", TARGET_EC2_TAG_VALUE); 18 | logger.info("Vulnerability severities are ", VULNERABILITY_SEVERITIES); 19 | logger.info("Override findings for target instances IDs are ", OVERRIDE_FINDINGS_FOR_TARGET_INSTANCES_IDS); 20 | 21 | try { 22 | let ecsManagedInstanceIds = []; 23 | if (OVERRIDE_FINDINGS_FOR_TARGET_INSTANCES_IDS && OVERRIDE_FINDINGS_FOR_TARGET_INSTANCES_IDS.length > 0) { 24 | ecsManagedInstanceIds = OVERRIDE_FINDINGS_FOR_TARGET_INSTANCES_IDS; 25 | } else { 26 | ecsManagedInstanceIds = await getECSManagedInstances(REGION, TARGET_EC2_TAG_NAME, TARGET_EC2_TAG_VALUE); 27 | } 28 | 29 | const { targetInstances, totalFindings } = await manageInstanceFindings(ecsManagedInstanceIds, REGION, VULNERABILITY_SEVERITIES); 30 | 31 | if (!targetInstances.length) { 32 | return { 33 | status: "No Finding of the set severity found.", 34 | patchedInstances: 0, 35 | statusCode: 200, 36 | }; 37 | } 38 | 39 | const ssm = new AWS.SSM({ region: REGION }); 40 | const logConfig = process.env.LAMBDA_LOG_GROUP ? { 41 | CloudWatchOutputConfig: { 42 | CloudWatchLogGroupName: process.env.LAMBDA_LOG_GROUP, 43 | CloudWatchOutputEnabled: true 44 | } 45 | } : {}; 46 | const patchResult = await ssm 47 | .sendCommand({ 48 | ...logConfig, 49 | Comment: 'Lambda function trigger operation for inspector finding auto-remediation', 50 | DocumentName: "AWS-RunPatchBaseline", 51 | DocumentVersion: "1", 52 | MaxConcurrency: `${targetInstances.length}`, 53 | MaxErrors: "0", 54 | OutputS3Region: "us-east-1", 55 | Parameters: { 56 | Operation: ["Install"], 57 | RebootOption: [REBOOT_OPTION] 58 | }, 59 | Targets: [ 60 | { 61 | Key: "InstanceIds", 62 | Values: targetInstances 63 | } 64 | ], 65 | TimeoutSeconds: 600 66 | }) 67 | .promise(); 68 | 69 | logger.info( 70 | `Remediation started for instances ${targetInstances.join(',')} with command ID: ${patchResult.Command.CommandId} 71 | and total findings of ${totalFindings}.` 72 | ); 73 | 74 | return { 75 | status: "Success", 76 | patchedInstances: targetInstances, 77 | statusCode: 200, 78 | }; 79 | } catch (error) { 80 | logger.error("Error during remediation:", error); 81 | throw error; 82 | } 83 | }; 84 | 85 | async function getECSManagedInstances(region, tagName, tagValue) { 86 | const params = { 87 | Filters: [ 88 | { 89 | Name: `tag:${tagName}`, 90 | Values: [tagValue], 91 | }, 92 | ], 93 | }; 94 | 95 | const ec2 = new AWS.EC2({ region }); 96 | 97 | let instanceIds = []; 98 | let data; 99 | do { 100 | data = await ec2.describeInstances(params).promise(); 101 | for (const reservation of data.Reservations) { 102 | for (const instance of reservation.Instances) { 103 | instanceIds.push(instance.InstanceId); 104 | } 105 | } 106 | params.NextToken = data.NextToken; 107 | } while (params.NextToken); 108 | 109 | return instanceIds; 110 | } 111 | 112 | async function getInstanceInspectorFindings(instanceId, region, severities) { 113 | const inspector2 = new AWS.Inspector2({ region }); 114 | 115 | const params = { 116 | sortCriteria: { 117 | sortOrder: "DESC", 118 | field: "SEVERITY", 119 | }, 120 | maxResults: 100, 121 | filterCriteria: { 122 | severity: severities.map(severity => ({ comparison: "EQUALS", value: severity })), 123 | resourceId: [{ comparison: "EQUALS", value: instanceId }], 124 | findingStatus: [{ comparison: "EQUALS", value: "ACTIVE" }], 125 | }, 126 | }; 127 | 128 | const data = await inspector2.listFindings(params).promise(); 129 | return data.findings; 130 | } 131 | 132 | async function manageInstanceFindings(ecsManagedInstanceIds, region, severities) { 133 | const targetInstances = []; 134 | let totalFindings = 0; 135 | 136 | for (const instanceId of ecsManagedInstanceIds) { 137 | logger.info("instanceId...", instanceId); 138 | let findings = await getInstanceInspectorFindings(instanceId, region, severities); 139 | if (!findings.length) { 140 | continue; 141 | } 142 | logger.info('Total findings for instanceID ' + instanceId + ' is ' + findings.length); 143 | 144 | const skippingTitles = ["Unsupported Operating System or Version", "No potential security issues found"]; 145 | findings = findings.filter((finding) => !(skippingTitles.includes(finding.title))) 146 | logger.info('Total findings for instanceID ' + instanceId + ' after title check is ' + findings.length); 147 | 148 | 149 | const resource = findings[0].resources[0]; 150 | 151 | // Check resource type to match expected remediation action: 152 | if (resource.type === "AWS_EC2_INSTANCE") { 153 | totalFindings = totalFindings += findings.length; 154 | targetInstances.push(instanceId); 155 | } 156 | } 157 | 158 | 159 | return { targetInstances, totalFindings } 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vulne-soldier: A Modern Day AWS EC2 Vulnerability Remediation Tool 2 | 3 | [![Terraform registry](https://img.shields.io/badge/Terraform_Registry-0.0.2-blue)](https://registry.terraform.io/modules/iKnowJavaScript/vulne-soldier/aws/latest) 4 | [![Terraform](https://img.shields.io/badge/Terraform-0.0.2-623CE4)](https://www.terraform.io) 5 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 6 | 7 | This Terraform module consists of the configuration for automating the remediation of AWS EC2 vulnerabilities using AWS Inspector findings. It provisions essential resources such as an SSM document, Lambda function, and CloudWatch event rules for automated vulnerability management. 8 | 9 | ## Prerequisites 10 | 11 | > **Important** 12 | > 13 | > The AWS Systems Manager (SSM) agent **must be installed and running** on all EC2 instances you wish to remediate. Without SSM, this module cannot trigger remediation actions on your instances. 14 | 15 | ## Description 16 | 17 | This Terraform module sets up an automated vulnerability remediation environment optimized for production use. By creating an SSM document to define the remediation steps, setting up a Lambda function to execute the remediation, and establishing CloudWatch event rules to trigger the process based on AWS Inspector findings, the module offers a straightforward approach to managing EC2 vulnerabilities on AWS. 18 | 19 | This module provisions: 20 | 21 | - AWS SSM documents 22 | - AWS Lambda functions 23 | - AWS CloudWatch event rules 24 | - IAM roles and policies 25 | 26 | 27 | ![Architecture](assets/component-overview.png) 28 | 29 | ## Usage 30 | 31 | ### Setup terraform module 32 | 33 | #### Download lambda 34 | 35 | To apply the terraform module, the compiled lambdas (.zip files) need to be available locally. They can either be downloaded from the GitHub release page or built locally. 36 | 37 | > **Info** 38 | 39 | > The lambdas can be downloaded from the [release page](https://github.com/iKnowJavaScript/terraform-aws-vulne-soldier/releases) or by building the Lambda folder using Node. 40 | 41 | For local development you can build the lambdas at once using `/lambda` or individually using `npm zip`. 42 | 43 | ### Example Configuration 44 | 45 | To deploy the `vulne-soldier` module, you can use the following configuration in your Terraform setup: 46 | 47 | ```hcl 48 | module "remediation" { 49 | source = "../../" 50 | 51 | name = "vulne-soldier-compliance-remediate" 52 | environment = "dev" 53 | aws_region = "us-east-1" 54 | account_id = "2123232323" 55 | lambda_log_group = "/aws/lambda/vulne-soldier-compliance-remediate" 56 | path_to_lambda_zip = "./lambda.zip" 57 | remediation_options = { 58 | region = "us-east-1" 59 | reboot_option = "NoReboot" 60 | # You need to specify the tag name and value of the EC2 instances you want to remediate 61 | target_ec2_tag_name = "AmazonECSManaged" 62 | target_ec2_tag_value = "true" 63 | # You can specify the vulnerability severities to filter findings: default is CRITICAL and HIGH vulnerabilities 64 | vulnerability_severities = ["CRITICAL, HIGH"] 65 | override_findings_for_target_instances_ids = [] 66 | } 67 | remediation_schedule_days = ["15", "L"] # Schedule remediation on the 15th and last day of each month 68 | ssm_notification_topic_arn = null # Optional: Specify an SNS topic ARN to receive notifications for remediation events 69 | } 70 | 71 | provider "aws" { 72 | region = "us-east-1" 73 | } 74 | ``` 75 | 76 | ### Triggers Remediation Process 77 | ![Vulnerability Remediation Trigger](assets/document-screenshot.png) 78 | On successful deployment, navigate to the AWS Systems Manager console and search for the SSM document created by the module (vulne-soldier-compliance-remediate-inspector-findings) or similar. You can trigger the remediation process by running the document on the affected EC2 instances. You can also create an AWS CloudWatch event rule to automate the process based on AWS Inspector findings. 79 | 80 | 81 | ## What's New in v2 82 | 83 | - Remediation is now **automated** using EventBridge rules, running by default with the `NoReboot` option for minimal disruption. You can update this option as needed in your configuration. 84 | 85 | ## Walkthrough Video 86 | 87 | [![v2 Walkthrough Demo](assets/v2-walkthrough.png)](https://vimeo.com/1098910908?share=copy#t=3.684) 88 | 89 | > Watch the [v2 walkthrough video](https://vimeo.com/1098910908?share=copy#t=3.684) for a step-by-step demonstration of setup and usage. 90 | 91 | ## Inputs 92 | 93 | | Name | Description | Type | Default | Required | 94 | |------------------------------------------|-----------------------------------------------------------------------------|---------------|--------------------------------------------|:--------:| 95 | | `name` | Name of the application | `string` | n/a | yes | 96 | | `environment` | Name of the environment | `string` | n/a | yes | 97 | | `aws_region` | AWS region where the resources will be created | `string` | n/a | yes | 98 | | `account_id` | AWS account ID | `string` | n/a | yes | 99 | | `lambda_log_group` | Name of the CloudWatch Log Group for the Lambda function | `string` | n/a | yes | 100 | | `path_to_lambda_zip` | File location of the lambda zip file for remediation | `string` | `lambda.zip` | yes | 101 | | `remediation_options` | Options for the remediation document | `object list` | n/a | yes | 102 | | `remediation_options.region` | The region to use | `string` | `us-east-1` | no | 103 | | `remediation_options.reboot_option` | Reboot option for patching | `string` | `NoReboot` | no | 104 | | `remediation_options.target_ec2_tag_name`| The tag name to filter EC2 instances | `string` | `AmazonECSManaged` | no | 105 | | `remediation_options.target_ec2_tag_value`| The tag value to filter EC2 instances | `string` | `true` | no | 106 | | `remediation_options.vulnerability_severities`| Comma separated list of vulnerability severities to filter findings | `string`| `"CRITICAL, HIGH"` | no | 107 | | `remediation_options.override_findings_for_target_instances_ids`| Comma separated list of instance IDs to override findings for target instances | `string`| `""` | no | 108 | | `remediation_schedule_days` | Days of the month to schedule remediation (e.g., ["15", "L"]) | `list(string)`| `["15", "L"]` | no | 109 | | `ssm_notification_topic_arn` | SNS topic ARN to receive notifications for remediation events (optional) | `string` | `null` | no | 110 | 111 | ## Outputs 112 | 113 | | Name | Description | Sensitive | 114 | |-----------------------|------------------------------|:---------:| 115 | | `lambda_function_arn` | Lambda function ARN | No | 116 | | `lambda_function_name`| Lambda function name | No | 117 | | `ssm_document_name` | SSM document name | No | 118 | 119 | To retrieve outputs, use the `terraform output` command, for example: `terraform output lambda_function_arn`. 120 | 121 | ## License 122 | 123 | This project is licensed under the MIT License - see the LICENSE.md file for details. -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.aws_region 3 | } 4 | 5 | locals { 6 | function_name = "${var.name}-${var.environment}" 7 | ssm_document_name = "${var.name}-inspector-findings-${var.environment}" 8 | lambda_zip = var.path_to_lambda_zip 9 | } 10 | 11 | resource "aws_ssm_document" "remediation_document" { 12 | name = local.ssm_document_name 13 | document_type = "Automation" 14 | 15 | content = < item 100 | } 101 | rule = aws_cloudwatch_event_rule.inspector_findings_schedule[each.value.schedule_idx].name 102 | target_id = "SSMVulneRemediationTarget-${each.value.opt_idx}-${each.value.schedule_idx}-${each.value.opt.region}" 103 | arn = aws_ssm_document.remediation_document.arn 104 | input = jsonencode({ 105 | region = each.value.opt.region, 106 | rebootOption = each.value.opt.reboot_option, 107 | targetEC2TagName = each.value.opt.target_ec2_tag_name, 108 | targetEC2TagValue = each.value.opt.target_ec2_tag_value, 109 | vulnerabilitySeverities = each.value.opt.vulnerability_severities, 110 | overrideFindingsForTargetInstancesIDs = each.value.opt.override_findings_for_target_instances_ids, 111 | }) 112 | role_arn = aws_iam_role.ssm_role.arn 113 | } 114 | 115 | resource "aws_cloudwatch_event_target" "sns_inspector_alert_scheduled" { 116 | count = var.ssn_notification_topic_arn != null ? length(local.remediation_schedule_crons) : 0 117 | rule = aws_cloudwatch_event_rule.inspector_findings_schedule[count.index].name 118 | target_id = "InspectorCriticalHighAlertsSNS-${count.index}-${var.environment}" 119 | arn = var.ssn_notification_topic_arn 120 | } 121 | 122 | resource "aws_iam_role" "ssm_role" { 123 | name = "SSMVulneAutomationRole" 124 | 125 | assume_role_policy = jsonencode({ 126 | Version = "2012-10-17", 127 | Statement = [ 128 | { 129 | Action = "sts:AssumeRole", 130 | Effect = "Allow", 131 | Principal = { 132 | Service = "events.amazonaws.com" 133 | } 134 | } 135 | ] 136 | }) 137 | } 138 | 139 | resource "aws_iam_role_policy" "ssm_document_execution" { 140 | name = "SSMDocumentExecution" 141 | role = aws_iam_role.ssm_role.id 142 | 143 | policy = jsonencode({ 144 | Version = "2012-10-17", 145 | Statement = concat( 146 | [ 147 | { 148 | Action = [ 149 | "ssm:StartAutomationExecution", 150 | "ssm:GetAutomationExecution" 151 | ], 152 | Effect = "Allow", 153 | Resource = "arn:aws:ssm:*:*:document/${local.ssm_document_name}*" 154 | } 155 | ], 156 | var.ssn_notification_topic_arn != null ? [ 157 | { 158 | Effect = "Allow", 159 | Action = "SNS:Publish", 160 | Resource = var.ssn_notification_topic_arn 161 | } 162 | ] : [] 163 | ) 164 | }) 165 | } 166 | 167 | resource "aws_iam_role" "lambda_execution_role" { 168 | name = "compliance-vulne-remediate_lambda_execution_role" 169 | 170 | assume_role_policy = jsonencode({ 171 | Version = "2012-10-17", 172 | Statement = [{ 173 | Action = "sts:AssumeRole", 174 | Effect = "Allow", 175 | Principal = { 176 | Service = "lambda.amazonaws.com" 177 | }, 178 | }], 179 | }) 180 | } 181 | 182 | resource "aws_iam_role_policy" "lambda_policy" { 183 | name = "compliance-vulne-remediate_lambda_policy" 184 | role = aws_iam_role.lambda_execution_role.id 185 | 186 | policy = jsonencode({ 187 | Version = "2012-10-17", 188 | Statement = [{ 189 | Action = [ 190 | "logs:CreateLogGroup", 191 | "logs:CreateLogStream", 192 | "logs:PutLogEvents" 193 | ], 194 | Effect = "Allow", 195 | Resource = "arn:aws:logs:*:${var.account_id}:*" 196 | }, 197 | { 198 | Effect = "Allow", 199 | Action = [ 200 | "ssm:StartAutomationExecution", 201 | "ssm:DescribeAutomationExecutions", 202 | "ssm:GetAutomationExecution" 203 | ], 204 | Resource = "arn:aws:ssm:*:${var.account_id}:automation-definition/*" 205 | }, 206 | { 207 | "Effect" : "Allow", 208 | "Action" : [ 209 | "ec2:DescribeInstances" 210 | ], 211 | "Resource" : "*" 212 | }, 213 | { 214 | Effect = "Allow", 215 | Action = [ 216 | "ssm:SendCommand" 217 | ], 218 | Resource = [ 219 | "*", 220 | "arn:aws:ec2:*:${var.account_id}:instance/*" 221 | ] 222 | }, 223 | { 224 | "Effect" : "Allow", 225 | "Action" : [ 226 | "inspector2:DescribeFindings", 227 | "inspector2:ListFindings", 228 | "inspector2:updateFindings" 229 | ], 230 | "Resource" : "arn:aws:inspector2:*:${var.account_id}:*" 231 | }, 232 | { 233 | "Effect" : "Allow", 234 | "Action" : [ 235 | "inspector:DescribeFindings", 236 | "inspector:ListFindings" 237 | ], 238 | "Resource" : "arn:aws:inspector:*:${var.account_id}:*" 239 | }], 240 | }) 241 | } 242 | 243 | 244 | resource "aws_lambda_function" "inspector_remediation" { 245 | filename = local.lambda_zip # You should zip your lambda_function.js before deploying 246 | function_name = local.function_name 247 | role = aws_iam_role.lambda_execution_role.arn 248 | handler = "index.handler" 249 | runtime = "nodejs20.x" 250 | source_code_hash = filebase64sha256(local.lambda_zip) 251 | 252 | timeout = 300 253 | 254 | environment { 255 | variables = { 256 | LOG_LEVEL = "INFO" 257 | LAMBDA_LOG_GROUP = var.lambda_log_group 258 | } 259 | } 260 | 261 | tags = { 262 | Environment = var.environment 263 | } 264 | tracing_config { 265 | mode = "Active" 266 | } 267 | } -------------------------------------------------------------------------------- /lambda/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manual-compliance-remediate-lambda-function", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "manual-compliance-remediate-lambda-function", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "aws-sdk": "^2.1531.0" 13 | }, 14 | "engines": { 15 | "node": "18", 16 | "npm": "9" 17 | } 18 | }, 19 | "node_modules/available-typed-arrays": { 20 | "version": "1.0.5", 21 | "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", 22 | "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", 23 | "engines": { 24 | "node": ">= 0.4" 25 | }, 26 | "funding": { 27 | "url": "https://github.com/sponsors/ljharb" 28 | } 29 | }, 30 | "node_modules/aws-sdk": { 31 | "version": "2.1532.0", 32 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1532.0.tgz", 33 | "integrity": "sha512-4QVQs01LEAxo7UpSHlq/HaO+SJ1WrYF8W1otO2WhKpVRYXkSxXIgZgfYaK+sQ762XTtB6tSuD2ZS2HGsKNXVLw==", 34 | "dependencies": { 35 | "buffer": "4.9.2", 36 | "events": "1.1.1", 37 | "ieee754": "1.1.13", 38 | "jmespath": "0.16.0", 39 | "querystring": "0.2.0", 40 | "sax": "1.2.1", 41 | "url": "0.10.3", 42 | "util": "^0.12.4", 43 | "uuid": "8.0.0", 44 | "xml2js": "0.5.0" 45 | }, 46 | "engines": { 47 | "node": ">= 10.0.0" 48 | } 49 | }, 50 | "node_modules/base64-js": { 51 | "version": "1.5.1", 52 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 53 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 54 | "funding": [ 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/feross" 58 | }, 59 | { 60 | "type": "patreon", 61 | "url": "https://www.patreon.com/feross" 62 | }, 63 | { 64 | "type": "consulting", 65 | "url": "https://feross.org/support" 66 | } 67 | ] 68 | }, 69 | "node_modules/buffer": { 70 | "version": "4.9.2", 71 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 72 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 73 | "dependencies": { 74 | "base64-js": "^1.0.2", 75 | "ieee754": "^1.1.4", 76 | "isarray": "^1.0.0" 77 | } 78 | }, 79 | "node_modules/call-bind": { 80 | "version": "1.0.5", 81 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", 82 | "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", 83 | "dependencies": { 84 | "function-bind": "^1.1.2", 85 | "get-intrinsic": "^1.2.1", 86 | "set-function-length": "^1.1.1" 87 | }, 88 | "funding": { 89 | "url": "https://github.com/sponsors/ljharb" 90 | } 91 | }, 92 | "node_modules/define-data-property": { 93 | "version": "1.1.1", 94 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", 95 | "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", 96 | "dependencies": { 97 | "get-intrinsic": "^1.2.1", 98 | "gopd": "^1.0.1", 99 | "has-property-descriptors": "^1.0.0" 100 | }, 101 | "engines": { 102 | "node": ">= 0.4" 103 | } 104 | }, 105 | "node_modules/events": { 106 | "version": "1.1.1", 107 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 108 | "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", 109 | "engines": { 110 | "node": ">=0.4.x" 111 | } 112 | }, 113 | "node_modules/for-each": { 114 | "version": "0.3.3", 115 | "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", 116 | "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", 117 | "dependencies": { 118 | "is-callable": "^1.1.3" 119 | } 120 | }, 121 | "node_modules/function-bind": { 122 | "version": "1.1.2", 123 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 124 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 125 | "funding": { 126 | "url": "https://github.com/sponsors/ljharb" 127 | } 128 | }, 129 | "node_modules/get-intrinsic": { 130 | "version": "1.2.2", 131 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", 132 | "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", 133 | "dependencies": { 134 | "function-bind": "^1.1.2", 135 | "has-proto": "^1.0.1", 136 | "has-symbols": "^1.0.3", 137 | "hasown": "^2.0.0" 138 | }, 139 | "funding": { 140 | "url": "https://github.com/sponsors/ljharb" 141 | } 142 | }, 143 | "node_modules/gopd": { 144 | "version": "1.0.1", 145 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 146 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 147 | "dependencies": { 148 | "get-intrinsic": "^1.1.3" 149 | }, 150 | "funding": { 151 | "url": "https://github.com/sponsors/ljharb" 152 | } 153 | }, 154 | "node_modules/has-property-descriptors": { 155 | "version": "1.0.1", 156 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", 157 | "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", 158 | "dependencies": { 159 | "get-intrinsic": "^1.2.2" 160 | }, 161 | "funding": { 162 | "url": "https://github.com/sponsors/ljharb" 163 | } 164 | }, 165 | "node_modules/has-proto": { 166 | "version": "1.0.1", 167 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 168 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 169 | "engines": { 170 | "node": ">= 0.4" 171 | }, 172 | "funding": { 173 | "url": "https://github.com/sponsors/ljharb" 174 | } 175 | }, 176 | "node_modules/has-symbols": { 177 | "version": "1.0.3", 178 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 179 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 180 | "engines": { 181 | "node": ">= 0.4" 182 | }, 183 | "funding": { 184 | "url": "https://github.com/sponsors/ljharb" 185 | } 186 | }, 187 | "node_modules/has-tostringtag": { 188 | "version": "1.0.0", 189 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", 190 | "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", 191 | "dependencies": { 192 | "has-symbols": "^1.0.2" 193 | }, 194 | "engines": { 195 | "node": ">= 0.4" 196 | }, 197 | "funding": { 198 | "url": "https://github.com/sponsors/ljharb" 199 | } 200 | }, 201 | "node_modules/hasown": { 202 | "version": "2.0.0", 203 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 204 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 205 | "dependencies": { 206 | "function-bind": "^1.1.2" 207 | }, 208 | "engines": { 209 | "node": ">= 0.4" 210 | } 211 | }, 212 | "node_modules/ieee754": { 213 | "version": "1.1.13", 214 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 215 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 216 | }, 217 | "node_modules/inherits": { 218 | "version": "2.0.4", 219 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 220 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 221 | }, 222 | "node_modules/is-arguments": { 223 | "version": "1.1.1", 224 | "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", 225 | "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", 226 | "dependencies": { 227 | "call-bind": "^1.0.2", 228 | "has-tostringtag": "^1.0.0" 229 | }, 230 | "engines": { 231 | "node": ">= 0.4" 232 | }, 233 | "funding": { 234 | "url": "https://github.com/sponsors/ljharb" 235 | } 236 | }, 237 | "node_modules/is-callable": { 238 | "version": "1.2.7", 239 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", 240 | "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", 241 | "engines": { 242 | "node": ">= 0.4" 243 | }, 244 | "funding": { 245 | "url": "https://github.com/sponsors/ljharb" 246 | } 247 | }, 248 | "node_modules/is-generator-function": { 249 | "version": "1.0.10", 250 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", 251 | "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", 252 | "dependencies": { 253 | "has-tostringtag": "^1.0.0" 254 | }, 255 | "engines": { 256 | "node": ">= 0.4" 257 | }, 258 | "funding": { 259 | "url": "https://github.com/sponsors/ljharb" 260 | } 261 | }, 262 | "node_modules/is-typed-array": { 263 | "version": "1.1.12", 264 | "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", 265 | "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", 266 | "dependencies": { 267 | "which-typed-array": "^1.1.11" 268 | }, 269 | "engines": { 270 | "node": ">= 0.4" 271 | }, 272 | "funding": { 273 | "url": "https://github.com/sponsors/ljharb" 274 | } 275 | }, 276 | "node_modules/isarray": { 277 | "version": "1.0.0", 278 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 279 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 280 | }, 281 | "node_modules/jmespath": { 282 | "version": "0.16.0", 283 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", 284 | "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", 285 | "engines": { 286 | "node": ">= 0.6.0" 287 | } 288 | }, 289 | "node_modules/punycode": { 290 | "version": "1.3.2", 291 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 292 | "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" 293 | }, 294 | "node_modules/querystring": { 295 | "version": "0.2.0", 296 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 297 | "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", 298 | "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", 299 | "engines": { 300 | "node": ">=0.4.x" 301 | } 302 | }, 303 | "node_modules/sax": { 304 | "version": "1.2.1", 305 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 306 | "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" 307 | }, 308 | "node_modules/set-function-length": { 309 | "version": "1.1.1", 310 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", 311 | "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", 312 | "dependencies": { 313 | "define-data-property": "^1.1.1", 314 | "get-intrinsic": "^1.2.1", 315 | "gopd": "^1.0.1", 316 | "has-property-descriptors": "^1.0.0" 317 | }, 318 | "engines": { 319 | "node": ">= 0.4" 320 | } 321 | }, 322 | "node_modules/url": { 323 | "version": "0.10.3", 324 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 325 | "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", 326 | "dependencies": { 327 | "punycode": "1.3.2", 328 | "querystring": "0.2.0" 329 | } 330 | }, 331 | "node_modules/util": { 332 | "version": "0.12.5", 333 | "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", 334 | "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", 335 | "dependencies": { 336 | "inherits": "^2.0.3", 337 | "is-arguments": "^1.0.4", 338 | "is-generator-function": "^1.0.7", 339 | "is-typed-array": "^1.1.3", 340 | "which-typed-array": "^1.1.2" 341 | } 342 | }, 343 | "node_modules/uuid": { 344 | "version": "8.0.0", 345 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", 346 | "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", 347 | "bin": { 348 | "uuid": "dist/bin/uuid" 349 | } 350 | }, 351 | "node_modules/which-typed-array": { 352 | "version": "1.1.13", 353 | "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", 354 | "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", 355 | "dependencies": { 356 | "available-typed-arrays": "^1.0.5", 357 | "call-bind": "^1.0.4", 358 | "for-each": "^0.3.3", 359 | "gopd": "^1.0.1", 360 | "has-tostringtag": "^1.0.0" 361 | }, 362 | "engines": { 363 | "node": ">= 0.4" 364 | }, 365 | "funding": { 366 | "url": "https://github.com/sponsors/ljharb" 367 | } 368 | }, 369 | "node_modules/xml2js": { 370 | "version": "0.5.0", 371 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 372 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 373 | "dependencies": { 374 | "sax": ">=0.6.0", 375 | "xmlbuilder": "~11.0.0" 376 | }, 377 | "engines": { 378 | "node": ">=4.0.0" 379 | } 380 | }, 381 | "node_modules/xmlbuilder": { 382 | "version": "11.0.1", 383 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 384 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 385 | "engines": { 386 | "node": ">=4.0" 387 | } 388 | } 389 | } 390 | } 391 | --------------------------------------------------------------------------------