├── 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 | [](https://registry.terraform.io/modules/iKnowJavaScript/vulne-soldier/aws/latest)
4 | [](https://www.terraform.io)
5 | [](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 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------