├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── release.yml
├── .gitignore
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── ignoredesc.gif
├── media
├── tfsec.png
└── tfsec.svg
├── package.json
├── resources
├── critical.svg
├── dark
│ ├── download.svg
│ ├── help.svg
│ ├── refresh.svg
│ ├── run.svg
│ └── tfsec.svg
├── high.svg
├── light
│ ├── download.svg
│ ├── help.svg
│ ├── refresh.svg
│ ├── run.svg
│ └── tfsec.svg
├── low.svg
└── medium.svg
├── src
├── check_manager.ts
├── explorer
│ ├── check_helpview.ts
│ ├── check_result.ts
│ ├── issues_treeview.ts
│ ├── tfsec_treeitem.ts
│ └── utils.ts
├── extension.ts
├── ignore.ts
├── snippets
│ └── custom_checks.json
└── tfsec_wrapper.ts
├── tfsec-explorer-usage.gif
├── tfsec.png
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": "warn",
13 | "@typescript-eslint/semi": "warn",
14 | "curly": "warn",
15 | "eqeqeq": "warn",
16 | "no-throw-literal": "warn",
17 | "semi": "off"
18 | },
19 | "ignorePatterns": [
20 | "**/*.d.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: owenrumney
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Desktop (please complete the following information):**
14 | - OS: [e.g. Windows]
15 | - Tfsec Version [e.g. v0.62.0]
16 | - VSCode Version
17 |
18 | **Additional context**
19 | Add any other context about the problem here.
20 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: vsix-release
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 |
7 | jobs:
8 | build:
9 | name: releasing vsix for extension
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout latest
14 | uses: actions/checkout@v2
15 |
16 | - name: Run npm install
17 | run: npm install
18 |
19 | - name: run the package action
20 | uses: lannonbr/vsce-action@3.0.0
21 | with:
22 | args: "publish -p $VSCE_TOKEN"
23 | env:
24 | VSCE_TOKEN: ${{ secrets.VSCE_TOKEN }}
25 |
26 | - name: Create Release
27 | id: create_release
28 | uses: actions/create-release@v1
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | with:
32 | tag_name: ${{ github.ref }}
33 | release_name: ${{ github.ref }}
34 | draft: false
35 | prerelease: false
36 |
37 | - name: Attach artifact to release
38 | uses: actions/upload-release-asset@v1
39 | with:
40 | upload_url: ${{ steps.create_release.outputs.upload_url }}
41 | asset_path: tfsec.vsix
42 | asset_name: tfsec.vsix
43 | asset_content_type: application/vsix
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 | *.js
7 | package-lock.json
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | *.gif
2 | *.png
3 | !tfsec.png
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to the "tfsec" extension will be documented in this file.
4 |
5 | ### 1.11.0
6 | - Add findings to the Problems tab
7 |
8 | ### 1.10.1
9 | - Fix Windows filepaths
10 |
11 | ### 1.10.0
12 | - Fix issue with file path names in the explorer
13 | - Add context support for locally ignoring files and directories
14 |
15 | ### 1.9.0
16 | - Support new tfsec filesystem (relative path resolution)
17 | - Maintain support older versions of tfsec
18 |
19 | ### 1.8.0
20 | - Add snippets support
21 | - using `tfsec-check-file` in a yaml file to create custom check
22 | - using `tfsec-custom-check` in the existing check file to add a new custom check
23 | - Add icon on toolbar to get the version
24 |
25 | ### 1.7.5
26 | - Update the severity icons inline with Aqua colours
27 |
28 | ### 1.7.4
29 | - Fix the icons for Severity and tfsec checkname
30 |
31 | ### 1.7.3
32 | - Fix issue with tfsec `v1.0.0-rc.2`
33 |
34 | ### 1.7.2
35 | - Fix issue with glob
36 |
37 | ### 1.7.1
38 | - Minify the extension
39 |
40 | ### 1.7.0
41 | - Support multi folder workspaces
42 | - Save results in a folder with unique names
43 |
44 | ### 1.6.2
45 | - Refactor the runner to clean up extension code
46 | - clean up some redundant code
47 |
48 | ### 1.6.1
49 | - Prettify with nice icons
50 | -
51 | ### 1.6.0
52 | - Switch from ExecSync to Spawn for running tfsec
53 | - Don't show the output window so much, we know its there
54 | - Update mass ignores to always add a new line
55 |
56 | ### 1.5.0
57 | - Check for tfsec before running any commands
58 | - Add debug setting for richer output option
59 | - remove some redundant logging
60 |
61 | ### 1.4.0
62 | - Use output channel instead of terminal for better cross platform command support
63 | - Remove explicit run command and use refresh to update the list with a fresh run
64 | - Add ignore all severity
65 | - Fix the refresh after ignores have been completed
66 | - Add more information to the update output
67 |
68 | ### 1.3.1
69 | - Update the repository link
70 |
71 | ### 1.3.0
72 | - Remove dependency on codes resource for resolving legacy IDs
73 |
74 | ### 1.2.2
75 | - Add support for AVD ID
76 |
77 | ### 1.2.1
78 | - Update the logo to the AquaSecurity one
79 |
80 | ### 1.2.0
81 | - Restructure explorer to be by severity
82 | - Fix the Help view for the checks
83 | - Add "Ignore all" to ignore all instances of an issue
84 |
85 | ### 1.1.11
86 | - Add menu button to update tfsec from within vscode (post tfsec v0.39.39)
87 | - Add command to show the current version of tfsec running
88 |
89 | ### 1.1.10
90 | - Updating the codes to support latest tfsec
91 |
92 | ### 1.1.9
93 | - Handle deprecated checks better in the help window
94 |
95 | ### 1.1.8
96 | - Add setting to choose if auto running tfsec after ignore should happen
97 |
98 | ### 1.1.7
99 | - Reload the tree when tfsec is run
100 | - move single line ignores above issue
101 |
102 | ### 1.1.6
103 | - Add tfsec ignore on a same line when single line issue
104 | - add local check help to the Tfsec navigation pane
105 | - restructure code for easier disable of plugin
106 |
107 | ### 1.1.5
108 | - Only use a single terminal for tfsec, don't create a new one on each run
109 | - Add option on extension settings to turn off the ignore code resolution
110 |
111 | ### 1.1.4
112 | - add link to check page from explorer view
113 | - update the icon for the activity bar
114 |
115 | ### 1.1.3
116 | - clean up the path in the treeview to remove prefix
117 |
118 | ### 1.1.2
119 | - Add some configuration options
120 | - binary path override
121 | - deep searching
122 | - exclude downloaded modules
123 |
124 | ### 1.1.1
125 | - Fixing issues with Windows path
126 | - Switch to using --out for results file, Powershell piping seems to use UTF8 BOM which is tricky
127 |
128 | ### 1.1.0
129 | - TFsec Explorer
130 |
131 | ### 1.0.8
132 | - Fix and issue with the code resolution
133 | - Record the demo gif and add
134 | - Fix ignore placement
135 |
136 | ### 1.0.6
137 | - Add ignore functionality in the context menu.
138 |
139 | ### 1.0.5
140 | - Fix issue reloading the results file on recreation
141 |
142 | ### 1.0.4
143 | - Fix issue when run against workspace with no data
144 |
145 | ### 1.0.3
146 | - Add the treeview for current issues in the workspace
147 |
148 | ### 1.0.2
149 | - Restructuring the code
150 |
151 | ### 1.0.1
152 | - Fixes to the Readme for the marketplace page
153 |
154 | ### 1.0.0
155 | - Initial release of tfsec extension with ignore parsing
156 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 AquaSecurity
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.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | build:
4 | npm run-script esbuild
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tfsec
2 |
3 | 
4 |
5 | This VS Code extension is for [tfsec](https://aquasecurity.github.io/tfsec/latest). A static analysis security scanner for your Terraform code that discovers problems with your infrastructure before hackers do.
6 |
7 | ## Features
8 |
9 | ### Findings Explorer
10 | The Findings Explorer displays an an organised view the issues that have been found in the current workspace.
11 |
12 | The code runs tfsec in a VS Code integrated terminal so you can see the the output - when it is complete, press the refresh button to reload.
13 |
14 | Right clicking on an tfsec code will let you view the associated page on [https://aquasecurity.github.io/tfsec/latest](https://aquasecurity.github.io/tfsec/latest)
15 |
16 | Issues can be ignored by right clicking the location in the explorer and selecting `ignore this issue`.
17 |
18 | 
19 | ### Ignore Code Resolution
20 |
21 | Ignore codes will be automatically resolved and the description of the error will be displayed inline.
22 |
23 | 
24 |
25 | ### Ignoring filepaths
26 |
27 | In the Explorer view, you can right click on a folder or .tf file and select `Ignore path during tfsec runs`. This will pass the path to `--exclude-path` when running tfsec and is only applicable to this workspace on this machine.
28 |
29 | To remove ignores, edit the `tfsec.excludedPath` in the `.vscode/settings.json` file of the current workspace.
30 |
31 | ## Release Notes
32 |
33 | ### 1.11.0
34 | - Add findings to the Problems tab
35 |
36 | ### 1.10.1
37 | - Fix Windows filepaths
38 |
39 | ### 1.10.0
40 | - Fix issue with file path names in the explorer
41 | - Add context support for locally ignoring files and directories
42 |
43 | ### 1.9.0
44 | - Support new tfsec filesystem (relative path resolution)
45 | - Maintain support older versions of tfsec
46 |
47 | ### 1.8.0
48 | - Add snippets support
49 | - using `tfsec-check-file` in a yaml file to create custom check
50 | - using `tfsec-custom-check` in the existing check file to add a new custom check
51 | - Add icon on toolbar to get the version
52 |
53 | ### 1.7.5
54 | - Update the severity icons inline with Aqua colours
55 |
56 | ### 1.7.4
57 | - Fix the icons for Severity and tfsec checkname
58 |
59 | ### 1.7.3
60 | - Fix issue with tfsec `v1.0.0-rc.2`
61 |
62 | ### 1.7.2
63 | - Fix issue with glob
64 |
65 | ### 1.7.1
66 | - Minify the extension
67 |
68 | ### 1.7.0
69 | - Support multi folder workspaces
70 | - Save results in a folder with unique names
71 |
72 | ### 1.6.2
73 | - Refactor the runner to clean up extension code
74 | - clean up some redundant code
75 |
76 |
77 | #### See Change log for more information
--------------------------------------------------------------------------------
/ignoredesc.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquasecurity/vscode-tfsec/55b8d7ae7be501d6c23a77d0519ab7a001543a63/ignoredesc.gif
--------------------------------------------------------------------------------
/media/tfsec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquasecurity/vscode-tfsec/55b8d7ae7be501d6c23a77d0519ab7a001543a63/media/tfsec.png
--------------------------------------------------------------------------------
/media/tfsec.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tfsec",
3 | "displayName": "tfsec",
4 | "publisher": "tfsec",
5 | "description": "tfsec integration for Visual Studio Code",
6 | "version": "1.11.0",
7 | "engines": {
8 | "vscode": "^1.54.0"
9 | },
10 | "icon": "tfsec.png",
11 | "categories": [
12 | "Other",
13 | "Linters"
14 | ],
15 | "languages": [
16 | {
17 | "id": "terraform",
18 | "extensions": [
19 | ".tf",
20 | ".tf.json"
21 | ],
22 | "aliases": [
23 | "Terraform"
24 | ]
25 | }
26 | ],
27 | "activationEvents": [
28 | "onView:tfsec.issueview",
29 | "onLanguage:terraform",
30 | "onCommand:tfsec.run",
31 | "workspaceContains:**/*.tf"
32 | ],
33 | "main": "./out/main.js",
34 | "contributes": {
35 | "configuration": {
36 | "title": "tfsec",
37 | "properties": {
38 | "tfsec.binaryPath": {
39 | "type": "string",
40 | "default": "tfsec",
41 | "description": "Path to tfsec if not already on the PATH"
42 | },
43 | "tfsec.ignoreDownloadedModules": {
44 | "type": "boolean",
45 | "default": "true",
46 | "description": "Don't include results from downloaded modules. (Still scanned, just not included in the results)"
47 | },
48 | "tfsec.fullDepthSearch": {
49 | "type": "boolean",
50 | "default": "true",
51 | "description": "Scan all terraform in the workspace. This will start at the top and add all terraform files into the model"
52 | },
53 | "tfsec.resolveIgnoreCodes": {
54 | "type": "boolean",
55 | "default": "true",
56 | "description": "Add the description for ignore codes inline with the code"
57 | },
58 | "tfsec.runOnIgnore": {
59 | "type": "boolean",
60 | "default": "true",
61 | "description": "Automatically rerun tfsec when a check failure is ignored"
62 | },
63 | "tfsec.debug": {
64 | "type": "boolean",
65 | "default": "false",
66 | "description": "Run tfsec with vebose flag to get more information"
67 | },
68 | "tfsec.excludedPaths": {
69 | "type": "array",
70 | "default": [],
71 | "description": "Run tfsec but exclude these folders"
72 | }
73 | }
74 | },
75 | "commands": [
76 | {
77 | "command": "tfsec.run",
78 | "title": "tfsec: Run tfsec against workspace",
79 | "icon": {
80 | "light": "resources/light/refresh.svg",
81 | "dark": "resources/dark/refresh.svg"
82 | }
83 | },
84 | {
85 | "command": "tfsec.refresh",
86 | "title": "tfsec: Refresh the issue list"
87 | },
88 | {
89 | "command": "tfsec.updatebinary",
90 | "title": "tfsec: Download the latest version of tfsec",
91 | "icon": {
92 | "light": "resources/light/download.svg",
93 | "dark": "resources/dark/download.svg"
94 | }
95 | },
96 | {
97 | "command": "tfsec.ignore",
98 | "title": "Ignore this issue instance"
99 | },
100 | {
101 | "command": "tfsec.ignorePath",
102 | "title": "Ignore path during tfsec runs"
103 | },
104 | {
105 | "command": "tfsec.ignoreAll",
106 | "title": "Ignore all instances"
107 | },
108 | {
109 | "command": "tfsec.ignoreSeverity",
110 | "title": "Ignore all instances of severity"
111 | },
112 | {
113 | "command": "tfsec.version",
114 | "title": "tfsec: Get the current version of tfsec",
115 | "icon": {
116 | "light": "resources/light/help.svg",
117 | "dark": "resources/dark/help.svg"
118 | }
119 | }
120 | ],
121 | "viewsContainers": {
122 | "activitybar": [
123 | {
124 | "id": "tfsec",
125 | "title": "tfsec",
126 | "icon": "media/tfsec.svg"
127 | }
128 | ]
129 | },
130 | "views": {
131 | "tfsec": [
132 | {
133 | "id": "tfsec.issueview",
134 | "name": "Findings Explorer",
135 | "icon": "media/tfsec.svg",
136 | "contextualTitle": "Findings Explorer"
137 | },
138 | {
139 | "id": "tfsec.helpview",
140 | "name": "Findings Help",
141 | "type": "webview",
142 | "contextualTitle": "Findings Help"
143 | }
144 | ]
145 | },
146 | "viewsWelcome": [
147 | {
148 | "view": "tfsec.issueview",
149 | "contents": "No issues are found.\n[Run tfsec now](command:tfsec.run)"
150 | },
151 | {
152 | "view": "tfsec.helpview",
153 | "contents": "No check selected. Run tfsec and choose a failed check from the explorer"
154 | }
155 | ],
156 | "menus": {
157 | "explorer/context": [
158 | {
159 | "command": "tfsec.ignorePath",
160 | "when": "resourceExtname == .tf || explorerResourceIsFolder"
161 | }
162 | ],
163 | "commandPalette": [
164 | {
165 | "command": "tfsec.ignore",
166 | "when": "false"
167 | }
168 | ],
169 | "view/title": [
170 | {
171 | "command": "tfsec.run",
172 | "when": "view == tfsec.issueview",
173 | "group": "navigation@1"
174 | },
175 | {
176 | "command": "tfsec.updatebinary",
177 | "when": "view == tfsec.issueview",
178 | "group": "navigation@2"
179 | },
180 | {
181 | "command": "tfsec.version",
182 | "when": "view == tfsec.issueview",
183 | "group": "navigation@2"
184 | }
185 | ],
186 | "view/item/context": [
187 | {
188 | "command": "tfsec.ignore",
189 | "when": "view == tfsec.issueview && viewItem == TFSEC_FILE_LOCATION"
190 | },
191 | {
192 | "command": "tfsec.ignoreAll",
193 | "when": "view == tfsec.issueview && viewItem == TFSEC_CODE"
194 | },
195 | {
196 | "command": "tfsec.ignoreSeverity",
197 | "when": "view == tfsec.issueview && viewItem == TFSEC_SEVERITY"
198 | }
199 | ]
200 | },
201 | "snippets": [
202 | {
203 | "language": "yaml",
204 | "path": "./src/snippets/custom_checks.json"
205 | }
206 | ]
207 | },
208 | "scripts": {
209 | "vscode:prepublish": "npm run esbuild-base -- --minify",
210 | "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node",
211 | "esbuild": "npm run esbuild-base -- --sourcemap",
212 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch",
213 | "test-compile": "tsc -p ./",
214 | "compile": "tsc -p ./",
215 | "pretest": "npm run compile && npm run lint",
216 | "lint": "eslint src --ext ts"
217 | },
218 | "devDependencies": {
219 | "@types/mocha": "^8.0.4",
220 | "@types/node": "^12.20.13",
221 | "@types/semver": "^7.3.6",
222 | "@types/vscode": "^1.54.0",
223 | "@typescript-eslint/eslint-plugin": "^4.25.0",
224 | "@typescript-eslint/parser": "^4.25.0",
225 | "esbuild": "^0.14.11",
226 | "eslint": "^7.27.0",
227 | "mocha": "^8.4.0",
228 | "typescript": "^4.5.4",
229 | "vscode-test": "^1.5.0"
230 | },
231 | "repository": {
232 | "url": "https://github.com/aquasecurity/vscode-tfsec"
233 | },
234 | "dependencies": {
235 | "@types/uuid": "^8.3.4",
236 | "semver": "^7.3.5",
237 | "typescipt": "^1.0.0",
238 | "uuid": "^8.3.2",
239 | "vsce": "^2.10.0"
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/resources/critical.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/resources/dark/download.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/dark/help.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/dark/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/dark/run.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/dark/tfsec.svg:
--------------------------------------------------------------------------------
1 |
2 |
106 |
--------------------------------------------------------------------------------
/resources/high.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/resources/light/download.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/light/help.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/resources/light/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources/light/run.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/resources/light/tfsec.svg:
--------------------------------------------------------------------------------
1 |
2 |
106 |
--------------------------------------------------------------------------------
/resources/low.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/resources/medium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/src/check_manager.ts:
--------------------------------------------------------------------------------
1 | export class CheckManager {
2 | private static instance: CheckManager;
3 | private loadedCodes = new Map([
4 | ["AWS001", "S3 Bucket has an ACL defined which allows public access."],
5 | ["AWS002", "S3 Bucket does not have logging enabled."],
6 | ["AWS003", "AWS Classic resource usage."],
7 | ["AWS004", "Use of plain HTTP."],
8 | ["AWS005", "Load balancer is exposed to the internet."],
9 | ["AWS006", "An ingress security group rule allows traffic from /0."],
10 | ["AWS007", "An egress security group rule allows traffic to /0."],
11 | ["AWS008", "An inline ingress security group rule allows traffic from /0."],
12 | ["AWS009", "An inline egress security group rule allows traffic to /0."],
13 | ["AWS010", "An outdated SSL policy is in use by a load balancer."],
14 | ["AWS011", "A database resource is marked as publicly accessible."],
15 | ["AWS012", "A resource has a public IP address."],
16 | ["AWS013", "Task definition defines sensitive environment variable(s)."],
17 | ["AWS014", "Launch configuration with unencrypted block device."],
18 | ["AWS015", "Unencrypted SQS queue."],
19 | ["AWS016", "Unencrypted SNS topic."],
20 | ["AWS017", "Unencrypted S3 bucket."],
21 | ["AWS018", "Missing description for security group/security group rule."],
22 | ["AWS019", "A KMS key is not configured to auto-rotate."],
23 | ["AWS020", "CloudFront distribution allows unencrypted (HTTP) communications."],
24 | ["AWS021", "CloudFront distribution uses outdated SSL/TLS protocols."],
25 | ["AWS022", "A MSK cluster allows unencrypted data in transit."],
26 | ["AWS023", "ECR repository has image scans disabled."],
27 | ["AWS024", "Kinesis stream is unencrypted."],
28 | ["AWS025", "API Gateway domain name uses outdated SSL/TLS protocols."],
29 | ["AWS031", "Elasticsearch domain isn't encrypted at rest."],
30 | ["AWS032", "Elasticsearch domain uses plaintext traffic for node to node communication."],
31 | ["AWS033", "Elasticsearch doesn't enforce HTTPS traffic."],
32 | ["AWS034", "Elasticsearch domain endpoint is using outdated TLS policy."],
33 | ["AWS035", "Unencrypted Elasticache Replication Group."],
34 | ["AWS036", "Elasticache Replication Group uses unencrypted traffic."],
35 | ["AWS037", "IAM Password policy should prevent password reuse."],
36 | ["AWS038", "IAM Password policy should have expiry less than or equal to 90 days."],
37 | ["AWS039", "IAM Password policy should have minimum password length of 14 or more characters."],
38 | ["AWS040", "IAM Password policy should have requirement for at least one symbol in the password."],
39 | ["AWS041", "IAM Password policy should have requirement for at least one number in the password."],
40 | ["AWS042", "IAM Password policy should have requirement for at least one lowercase character."],
41 | ["AWS043", "IAM Password policy should have requirement for at least one uppercase character."],
42 | ["AWS044", "AWS provider has access credentials specified."],
43 | ["AWS045", "CloudFront distribution does not have a WAF in front."],
44 | ["AWS046", "AWS IAM policy document has wildcard action statement."],
45 | ["AWS047", "AWS SQS policy document has wildcard action statement."],
46 | ["AWS048", "EFS Encryption has not been enabled"],
47 | ["AWS049", "An ingress Network ACL rule allows specific ports from /0."],
48 | ["AWS050", "An ingress Network ACL rule allows ALL ports from /0."],
49 | ["AWS051", "There is no encryption specified or encryption is disabled on the RDS Cluster."],
50 | ["AWS052", "RDS encryption has not been enabled at a DB Instance level."],
51 | ["AWS053", "Encryption for RDS Perfomance Insights should be enabled."],
52 | ["AWS057", "Domain logging should be enabled for Elastic Search domains"],
53 | ["AWS058", "Ensure that lambda function permission has a source arn specified"],
54 | ["AWS059", "Athena databases and workgroup configurations are created unencrypted at rest by default, they should be encrypted"],
55 | ["AWS060", "Athena workgroups should enforce configuration to prevent client disabling encryption"],
56 | ["AWS061", "API Gateway stages for V1 and V2 should have access logging enabled"],
57 | ["AWS062", "User data for EC2 instances must not contain sensitive AWS keys"],
58 | ["AWS063", "Cloudtrail should be enabled in all regions regardless of where your AWS resources are generally homed"],
59 | ["AWS064", "Cloudtrail log validation should be enabled to prevent tampering of log data"],
60 | ["AWS065", "Cloudtrail should be encrypted at rest to secure access to sensitive trail data"],
61 | ["AWS066", "EKS should have the encryption of secrets enabled"],
62 | ["AWS067", "EKS Clusters should have cluster control plane logging turned on"],
63 | ["AWS068", "EKS cluster should not have open CIDR range for public access"],
64 | ["AWS069", "EKS Clusters should have the public access disabled"],
65 | ["AWS070", "AWS ES Domain should have logging enabled"],
66 | ["AWS071", "Cloudfront distribution should have Access Logging configured"],
67 | ["AWS072", "Viewer Protocol Policy in Cloudfront Distribution Cache should always be set to HTTPS"],
68 | ["AWS073", "S3 Access Block should Ignore Public Acl"],
69 | ["AWS074", "S3 Access block should block public ACL"],
70 | ["AWS075", "S3 Access block should restrict public bucket to limit access"],
71 | ["AWS076", "S3 Access block should block public policy"],
72 | ["AWS077", "S3 Data should be versioned"],
73 | ["AWS078", "ECR images tags shouldn't be mutable."],
74 | ["AWS079", "aws_instance should activate session tokens for Instance Metadata Service."],
75 | ["AWS080", "CodeBuild Project artifacts encryption should not be disabled"],
76 | ["AWS081", "DAX Cluster should always encrypt data at rest"],
77 | ["AWS082", "It is AWS best practice to not use the default VPC for workflows"],
78 | ["AWS083", "Load balancers should drop invalid headers"],
79 | ["AWS084", "Root and user volumes on Workspaces should be encrypted"],
80 | ["AWS085", "Config configuration aggregator should be using all regions for source"],
81 | ["AWS086", "Point in time recovery should be enabled to protect DynamoDB table"],
82 | ["AWS087", "Redshift cluster should be deployed into a specific VPC"],
83 | ["AWS088", "Redis cluster should be backup retention turned on"],
84 | ["AWS089", "CloudWatch log groups should be encrypted"],
85 | ["AWS090", "ECS clusters should have container insights enabled"],
86 | ["AWS091", "RDS Cluster and RDS instance should have backup retention longer than default 1 day"],
87 | ["AWS092", "DynamoDB tables should use at rest encyption with a Customer Managed Key"],
88 | ["AWS093", "ECR Repository should use customer managed keys to allow more control"],
89 | ["AWS094", "Redshift clusters should use at rest encryption"],
90 | ["AWS095", "Secrets Manager should use customer managed keys"],
91 | ["AWS096", "ECS Task Definitions with EFS volumes should use in-transit encryption"],
92 | ["AWS097", "Document DB should be encrypted using customer managed keys"],
93 | ["AWS098", "Where supported use EBS optimization"],
94 | ["AZU001", "An inbound network security rule allows traffic from /0."],
95 | ["AZU002", "An outbound network security rule allows traffic to /0."],
96 | ["AZU003", "Unencrypted managed disk."],
97 | ["AZU004", "Unencrypted data lake storage."],
98 | ["AZU005", "Password authentication in use instead of SSH keys."],
99 | ["AZU006", "Ensure AKS cluster has Network Policy configured"],
100 | ["AZU007", "Ensure RBAC is enabled on AKS clusters"],
101 | ["AZU008", "Ensure AKS has an API Server Authorized IP Ranges enabled"],
102 | ["AZU009", "Ensure AKS logging to Azure Monitoring is Configured"],
103 | ["AZU010", "Ensure HTTPS is enabled on Azure Storage Account"],
104 | ["AZU011", "Storage containers in blob storage mode should not have public access"],
105 | ["AZU012", "The default action on Storage account network rules should be set to deny"],
106 | ["AZU013", "Trusted Microsoft Services should have bypass access to Storage accounts"],
107 | ["AZU014", "Storage accounts should be configured to only accept transfers that are over secure connections"],
108 | ["AZU015", "The minimum TLS version for Storage Accounts should be TLS1_2"],
109 | ["AZU016", "When using Queue Services for a storage account, logging should be enabled."],
110 | ["AZU017", "SSH access should not be accessible from the Internet, should be blocked on port 22"],
111 | ["AZU018", "Auditing should be enabled on Azure SQL Databases"],
112 | ["AZU019", "Database auditing rentention period should be longer than 90 days"],
113 | ["AZU020", "Key vault should have the network acl block specified"],
114 | ["AZU021", "Key vault should have purge protection enabled"],
115 | ["AZU022", "Key vault Secret should have a content type set"],
116 | ["AZU023", "Key Vault Secret should have an expiration date set"],
117 | ["AZU024", "RDP access should not be accessible from the Internet, should be blocked on port 3389"],
118 | ["AZU025", "Data Factory should have public access disabled, the default is enabled."],
119 | ["AZU026", "Ensure that the expiration date is set on all keys"],
120 | ["AZU027", "Synapse Workspace should have managed virtual network enabled, the default is disabled."],
121 | ["AZU028", "Ensure the Function App can only be accessed via HTTPS. The default is false."],
122 | ["GCP001", "Unencrypted compute disk."],
123 | ["GCP003", "An inbound firewall rule allows traffic from /0."],
124 | ["GCP004", "An outbound firewall rule allows traffic to /0."],
125 | ["GCP005", "Legacy ABAC permissions are enabled."],
126 | ["GCP006", "Node metadata value disables metadata concealment."],
127 | ["GCP007", "Legacy metadata endpoints enabled."],
128 | ["GCP008", "Legacy client authentication methods utilized."],
129 | ["GCP009", "Pod security policy enforcement not defined."],
130 | ["GCP010", "Shielded GKE nodes not enabled."],
131 | ["GCP011", "IAM granted directly to user."],
132 | ["GCP012", "Checks for service account defined for GKE nodes"],
133 | ["GEN001", "Potentially sensitive data stored in \"default \" value of variable."],
134 | ["GEN002", "Potentially sensitive data stored in local value."],
135 | ["GEN003", "Potentially sensitive data stored in block attribute."],
136 | ["GEN004", "Github repository shouldn't be public."]
137 | ]);
138 |
139 | private constructor() { };
140 |
141 | static getInstance(): CheckManager {
142 | if (!CheckManager.instance) {
143 | CheckManager.instance = new CheckManager();
144 | }
145 |
146 | return CheckManager.instance;
147 | };
148 |
149 | get(code: string): string | undefined {
150 | return this.loadedCodes.get(code);
151 | };
152 | };
--------------------------------------------------------------------------------
/src/explorer/check_helpview.ts:
--------------------------------------------------------------------------------
1 | import { CancellationToken, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext } from "vscode";
2 | import { CheckResult, CheckSeverity } from "./check_result";
3 | import { TfsecTreeItem } from "./tfsec_treeitem";
4 |
5 | export class TfsecHelpProvider implements WebviewViewProvider {
6 | private view: Webview | undefined;
7 |
8 | resolveWebviewView(webviewView: WebviewView, _context: WebviewViewResolveContext, _token: CancellationToken): void | Thenable {
9 | this.view = webviewView.webview;
10 | this.update(null);
11 | }
12 |
13 | update(item: TfsecTreeItem | null) {
14 | if (this.view === undefined) {
15 | return;
16 | }
17 | if (item === null) {
18 | return;
19 | }
20 | const codeData = item.check;
21 | if (codeData === undefined) {
22 | this.view.html = `
23 | No check data available
24 | This check may no longer be valid. Check your tfsec is the latest version.
25 | `;
26 | return;
27 | }
28 |
29 | if (item.contextValue === 'TFSEC_CODE') {
30 | this.view.html = `
31 | Select a specific instance for more details
32 | For more information about the issue found, select a specific instance.
33 | `;
34 | return;
35 | }
36 | this.view.html = getHtml(codeData);
37 | }
38 | }
39 |
40 | function getHtml(codeData: CheckResult | CheckSeverity | undefined): string {
41 | if (codeData === undefined || codeData instanceof CheckSeverity) {
42 | return "";
43 | }
44 | return `
45 | ${codeData?.codeDescription}
46 | ${codeData?.summary}
47 |
48 | ID
49 | ${codeData?.code}
50 |
51 | Severity
52 | ${codeData?.severity}
53 |
54 | Impact
55 | ${codeData?.impact}
56 |
57 | Resolution
58 | ${codeData?.resolution}
59 |
60 | Filename
61 | ${codeData?.filename}
62 |
63 | More Information
64 | ${codeData?.docUrl}
65 | `;
66 | }
67 |
--------------------------------------------------------------------------------
/src/explorer/check_result.ts:
--------------------------------------------------------------------------------
1 | import { capitalize } from './utils';
2 |
3 |
4 | export class CheckResult {
5 | public code: string;
6 | public provider: string;
7 | public codeDescription: string;
8 | public filename: string;
9 | public startLine: number;
10 | public endLine: number;
11 | public severity: string;
12 | public summary: string;
13 | public impact: string;
14 | public resolution: string;
15 | public docUrl: any;
16 | constructor(
17 | result: any
18 | ) {
19 | this.code = result.long_id ?? result.rule_id;
20 | this.provider = result.rule_provider;
21 | this.codeDescription = result.rule_description;
22 | this.summary = result.description;
23 | this.impact = result.impact;
24 | this.resolution = result.resolution;
25 | this.filename = result.location.filename;
26 | this.startLine = result.location.start_line;
27 | this.endLine = result.location.end_line;
28 | this.severity = capitalize(result.severity);
29 |
30 | if (result.links.length > 0) {
31 | this.docUrl = result.links[0];
32 | }
33 | }
34 | }
35 |
36 | export class CheckSeverity {
37 | public severity: string;
38 | constructor(result: any) {
39 | this.severity = capitalize(result.severity);
40 | }
41 | }
--------------------------------------------------------------------------------
/src/explorer/issues_treeview.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 | import { sortByCode, sortBySeverity, sortResults, uniqueLocations } from './utils';
5 | import { CheckResult, CheckSeverity } from './check_result';
6 | import { TfsecTreeItem, TfsecTreeItemType } from './tfsec_treeitem';
7 | import { checkServerIdentity } from 'tls';
8 |
9 | export class TfsecIssueProvider implements vscode.TreeDataProvider {
10 |
11 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
12 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
13 | public resultData: CheckResult[] = [];
14 | private taintResults: boolean = true;
15 | public rootpath: string = "";
16 | private storagePath: string = "";
17 | public readonly resultsStoragePath: string = "";
18 | private diagCollection: vscode.DiagnosticCollection;
19 |
20 | constructor(context: vscode.ExtensionContext, diagCollection: vscode.DiagnosticCollection) {
21 | if (context.storageUri) {
22 | this.storagePath = context.storageUri.fsPath;
23 | console.log(`storage path is ${this.storagePath}`);
24 | if (!fs.existsSync(this.storagePath)) {
25 | fs.mkdirSync(context.storageUri.fsPath);
26 | }
27 | this.resultsStoragePath = path.join(context.storageUri.fsPath, '/.tfsec/');
28 | if (!fs.existsSync(this.resultsStoragePath)) {
29 | fs.mkdirSync(this.resultsStoragePath);
30 | }
31 | }
32 |
33 | this.diagCollection = diagCollection;
34 | }
35 |
36 | refresh(): void {
37 | this.taintResults = true;
38 | this.loadResultData();
39 | }
40 |
41 | // when there is a tfsec output file, load the results
42 | async loadResultData() {
43 | var _self = this;
44 | _self.resultData = [];
45 | if (this.resultsStoragePath !== "" && vscode.workspace && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]) {
46 | this.rootpath = vscode.workspace.workspaceFolders[0].uri.fsPath;
47 | var files = fs.readdirSync(this.resultsStoragePath).filter(fn => fn.endsWith('_results.json') || fn.endsWith('_results.json.json'));
48 | Promise.resolve(files.forEach(file => {
49 | const resultFile = path.join(this.resultsStoragePath, file);
50 | if (fs.existsSync(resultFile)) {
51 | let content = fs.readFileSync(resultFile, 'utf8');
52 |
53 | let diagnostics = new Map();
54 |
55 | try {
56 | const data = JSON.parse(content);
57 | if (data === null || data.results === null) {
58 | return;
59 | }
60 | let results = data.results.sort(sortResults);
61 | for (let i = 0; i < results.length; i++) {
62 | const element = results[i];
63 | let result = new CheckResult(element);
64 | _self.resultData.push(result);
65 |
66 | if (diagnostics.get(result.filename) === undefined) {
67 | diagnostics.set(result.filename, []);
68 | }
69 | diagnostics.get(result.filename)?.push(this.processProblem(result));
70 | }
71 | }
72 | catch {
73 | console.debug(`Error loading results file ${file}`);
74 | }
75 |
76 | for (let [key, value] of diagnostics) {
77 | this.diagCollection.set(vscode.Uri.file(key), value)
78 | }
79 | }
80 | })).then(() => {
81 | _self.taintResults = !_self.taintResults;
82 | _self._onDidChangeTreeData.fire();
83 | });
84 | } else {
85 | vscode.window.showInformationMessage("No workspace detected to load tfsec results from");
86 | }
87 | this.taintResults = false;
88 | }
89 |
90 | getTreeItem(element: TfsecTreeItem): vscode.TreeItem {
91 | return element;
92 | }
93 |
94 | getChildren(element?: TfsecTreeItem): Thenable {
95 | // if this is refresh then get the top level codes
96 | let items: TfsecTreeItem[] = [];
97 | if (!element) {
98 | items = this.getCurrentTfsecSeverities();
99 | } else if (element.treeItemType === TfsecTreeItemType.issueSeverity) {
100 | items = this.getCurrentTfsecIssues(element.severity);
101 | } else {
102 | items = this.getIssuesLocationsByCode(element.code);
103 | }
104 | return Promise.resolve(items);
105 | }
106 |
107 | private getCurrentTfsecSeverities(): TfsecTreeItem[] {
108 | var results: TfsecTreeItem[] = [];
109 | var resolvedSeverities: string[] = [];
110 |
111 | for (let index = 0; index < this.resultData.length; index++) {
112 | const result = this.resultData[index];
113 | if (result === undefined) {
114 | continue;
115 | }
116 |
117 | if (resolvedSeverities.includes(result.severity)) {
118 | continue;
119 | }
120 | resolvedSeverities.push(result.severity);
121 | results.push(new TfsecTreeItem(result.severity, new CheckSeverity(result), vscode.TreeItemCollapsibleState.Collapsed));
122 | }
123 | return results.sort(sortBySeverity);
124 | }
125 |
126 |
127 | private getCurrentTfsecIssues(severity: string): TfsecTreeItem[] {
128 | var results: TfsecTreeItem[] = [];
129 | var resolvedCodes: string[] = [];
130 |
131 |
132 | for (let index = 0; index < this.resultData.length; index++) {
133 | const result = this.resultData[index];
134 |
135 | if (result === undefined) {
136 | continue;
137 | }
138 | if (resolvedCodes.includes(result.code) || result.severity !== severity) {
139 | continue;
140 | }
141 | resolvedCodes.push(result.code);
142 | results.push(new TfsecTreeItem(result.code, result, vscode.TreeItemCollapsibleState.Collapsed));
143 | }
144 | return results.sort(sortByCode);
145 | }
146 |
147 | getIssuesLocationsByCode(code: string): TfsecTreeItem[] {
148 | var results: TfsecTreeItem[] = [];
149 |
150 | const filtered = this.resultData.filter(c => c.code === code);
151 | for (let index = 0; index < filtered.length; index++) {
152 | const result = filtered[index];
153 |
154 | if (result === undefined) {
155 | continue;
156 | }
157 | if (result.code !== code) {
158 | continue;
159 | }
160 | const filename = path.relative(this.rootpath, result.filename);
161 | const cmd = this.createFileOpenCommand(result);
162 | var item = new TfsecTreeItem(`${filename}:${result.startLine}`, result, vscode.TreeItemCollapsibleState.None, cmd);
163 | results.push(item);
164 | }
165 | return uniqueLocations(results);
166 | }
167 |
168 |
169 | private createFileOpenCommand(result: CheckResult) {
170 | const issueRange = new vscode.Range(new vscode.Position(result.startLine - 1, 0), new vscode.Position(result.endLine, 0));
171 |
172 | const pathToOpen = path.join(result.filename);
173 |
174 | return {
175 | command: "vscode.open",
176 | title: "",
177 | arguments: [
178 | vscode.Uri.file(pathToOpen),
179 | {
180 | selection: issueRange,
181 | }
182 | ]
183 | };
184 | }
185 |
186 | private processProblem(check: CheckResult): vscode.Diagnostic {
187 | let severity = check.severity === "Critical" || check.severity === "High" || check.severity === "Medium" ? vscode.DiagnosticSeverity.Error : vscode.DiagnosticSeverity.Warning
188 | return new vscode.Diagnostic(new vscode.Range(new vscode.Position(check.startLine -1, 0), new vscode.Position(check.endLine -1, 0)), check.summary, severity);
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/explorer/tfsec_treeitem.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import { CheckResult, CheckSeverity } from './check_result';
4 |
5 | export class TfsecTreeItem extends vscode.TreeItem {
6 |
7 | treeItemType: TfsecTreeItemType;
8 | code: string;
9 | provider: string;
10 | startLineNumber: number;
11 | endLineNumber: number;
12 | filename: string;
13 | severity: string;
14 | contextValue = '';
15 |
16 | constructor(
17 | public readonly title: string,
18 | public readonly check: CheckResult | CheckSeverity,
19 | public collapsibleState: vscode.TreeItemCollapsibleState,
20 | public command?: vscode.Command,
21 | ) {
22 | super(title, collapsibleState);
23 | this.severity = check.severity;
24 | this.code = "";
25 | this.provider = "";
26 | this.startLineNumber = 0;
27 | this.endLineNumber = 0;
28 | this.filename = "";
29 |
30 | if (check instanceof CheckResult) {
31 | this.code = check.code;
32 | this.provider = check.provider;
33 | if (collapsibleState === vscode.TreeItemCollapsibleState.None) {
34 | this.treeItemType = TfsecTreeItemType.issueLocation;
35 | this.contextValue = "TFSEC_FILE_LOCATION";
36 | this.startLineNumber = check.startLine;
37 | this.endLineNumber = check.endLine;
38 | this.filename = check.filename;
39 | this.iconPath = vscode.ThemeIcon.File;
40 | this.resourceUri = vscode.Uri.parse(check.filename);
41 | } else {
42 | this.treeItemType = TfsecTreeItemType.issueCode;
43 | this.contextValue = "TFSEC_CODE";
44 | this.tooltip = `${check.codeDescription}`;
45 | this.iconPath = {
46 | light: path.join(__filename, '..', '..', 'resources', 'light', 'tfsec.svg'),
47 | dark: path.join(__filename, '..', '..', 'resources', 'dark', 'tfsec.svg')
48 | };
49 | }
50 | } else {
51 | this.treeItemType = TfsecTreeItemType.issueSeverity;
52 | this.contextValue = "TFSEC_SEVERITY";
53 | this.iconPath = {
54 | light: path.join(__filename, '..', '..', 'resources', this.severityIcon(this.severity)),
55 | dark: path.join(__filename, '..', '..', 'resources', this.severityIcon(this.severity))
56 | };
57 | }
58 | }
59 |
60 | severityIcon = (severity: string): string => {
61 | switch (severity) {
62 | case "Critical":
63 | return 'critical.svg';
64 | case "High":
65 | return 'high.svg';
66 | case "Medium":
67 | return 'medium.svg';
68 | case "Low":
69 | return 'low.svg';
70 | }
71 | return 'unknown.svg';
72 | };
73 | }
74 |
75 | export enum TfsecTreeItemType {
76 | issueCode = 0,
77 | issueLocation = 1,
78 | issueSeverity = 2,
79 | }
--------------------------------------------------------------------------------
/src/explorer/utils.ts:
--------------------------------------------------------------------------------
1 |
2 | import { TfsecTreeItem } from './tfsec_treeitem';
3 |
4 |
5 | function getSeverityPosition(severity: string): number {
6 | switch (severity) {
7 | case 'Critical':
8 | return 0;
9 | case 'High':
10 | return 1;
11 | case 'Medium':
12 | return 2;
13 | case 'Low':
14 | return 3;
15 | default:
16 | return -1;
17 | }
18 | }
19 |
20 | const sortByCode = (a: TfsecTreeItem, b: TfsecTreeItem): number => {
21 | if (a.code
22 | > b.code) {
23 | return 1;
24 | } else if (a.code < b.code) {
25 | return -1;
26 | }
27 | return 0;
28 | };
29 |
30 | const sortResults = (a: any, b: any): number => {
31 | if (a.filename > b.filename) {
32 | return 1;
33 | } else if (a.filename < b.filename) {
34 | return -1;
35 | }
36 | return 0;
37 | };
38 |
39 | const sortBySeverity = (a: TfsecTreeItem, b: TfsecTreeItem): number => {
40 | if (getSeverityPosition(a.severity) > getSeverityPosition(b.severity)) {
41 | return 1;
42 | } else if (getSeverityPosition(a.severity) < getSeverityPosition(b.severity)) {
43 | return -1;
44 | }
45 |
46 | return 0;
47 | };
48 |
49 | const sortByFilename = (a: TfsecTreeItem, b: TfsecTreeItem): number => {
50 | if (a.filename > b.filename) {
51 | return 1;
52 | } else if (a.filename < b.filename) {
53 | return -1;
54 | }
55 | return 0;
56 | };
57 |
58 | const sortByLineNumber = (a: TfsecTreeItem, b: TfsecTreeItem): number => {
59 | if (a.startLineNumber > b.startLineNumber) {
60 | return 1;
61 | } else if (a.startLineNumber < b.startLineNumber) {
62 | return -1;
63 | }
64 | return 0;
65 | };
66 |
67 | const uniqueLocations = (input: TfsecTreeItem[]): TfsecTreeItem[] => {
68 |
69 | if (input.length === 0) {
70 | return input;
71 | }
72 | input.sort(sortByLineNumber);
73 | let output: TfsecTreeItem[] = [];
74 | let last = input[0];
75 | if (last === undefined) {
76 | return [];
77 | }
78 | output.push(last);
79 |
80 | for (let index = 1; index < input.length; index++) {
81 | const element = input[index];
82 | if (element === undefined) {
83 | continue;
84 | }
85 | if (last?.code !== element?.code || last?.filename !== element?.filename || last?.startLineNumber !== element?.startLineNumber) {
86 | output.push(element);
87 | last = element;
88 | }
89 | }
90 |
91 | return output.sort(sortByFilename);
92 | };
93 |
94 |
95 |
96 | const capitalize = (s: string) => (s && s[0] && s[0].toUpperCase() + s.slice(1).toLowerCase()) || '';
97 |
98 | export { sortByCode, sortBySeverity, sortResults, uniqueLocations, capitalize };
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { ignoreAllInstances, ignoreInstance, ingorePath, triggerDecoration } from './ignore';
3 | import { TfsecIssueProvider } from './explorer/issues_treeview';
4 | import { TfsecTreeItem } from './explorer/tfsec_treeitem';
5 | import { TfsecHelpProvider } from './explorer/check_helpview';
6 |
7 | import { extname } from 'path';
8 | import { TfsecWrapper } from './tfsec_wrapper';
9 |
10 |
11 | // this method is called when vs code is activated
12 | export function activate(context: vscode.ExtensionContext) {
13 | let activeEditor = vscode.window.activeTextEditor;
14 | var outputChannel = vscode.window.createOutputChannel("tfsec");
15 | let diagCollection = vscode.languages.createDiagnosticCollection();
16 |
17 | const helpProvider = new TfsecHelpProvider();
18 | const issueProvider = new TfsecIssueProvider(context, diagCollection);
19 | const tfsecWrapper = new TfsecWrapper(outputChannel, issueProvider.resultsStoragePath);
20 |
21 | // creating the issue tree explicitly to allow access to events
22 | let issueTree = vscode.window.createTreeView("tfsec.issueview", {
23 | treeDataProvider: issueProvider,
24 | });
25 |
26 | issueTree.onDidChangeSelection(function (event) {
27 | const treeItem = event.selection[0];
28 | if (treeItem) {
29 | helpProvider.update(treeItem);
30 | }
31 | });
32 |
33 | context.subscriptions.push(vscode.window.registerWebviewViewProvider("tfsec.helpview", helpProvider));
34 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.refresh', () => issueProvider.refresh()));
35 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.version', () => tfsecWrapper.showCurrentTfsecVersion()));
36 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.ignore', (element: TfsecTreeItem) => ignoreInstance(element, outputChannel)));
37 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.ignoreAll', (element: TfsecTreeItem) => ignoreAllInstances(element, issueProvider, outputChannel)));
38 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.ignoreSeverity', (element: TfsecTreeItem) => ignoreAllInstances(element, issueProvider, outputChannel)));
39 | context.subscriptions.push(vscode.commands.registerCommand("tfsec.run", () => tfsecWrapper.run()));
40 | context.subscriptions.push(vscode.commands.registerCommand("tfsec.updatebinary", () => tfsecWrapper.updateBinary()));
41 | context.subscriptions.push(vscode.commands.registerCommand('tfsec.ignorePath', (element: any) => ingorePath(element)));
42 |
43 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(editor => {
44 | // only act if this is a terraform file
45 | if (editor && extname(editor.document.fileName) !== '.tf') {
46 | return;
47 | }
48 | activeEditor = editor;
49 | triggerDecoration();
50 | }, null, context.subscriptions));
51 |
52 | context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => {
53 | // only act if this is a terraform file
54 | if (extname(event.document.fileName) !== '.tf') {
55 | return;
56 | }
57 | if (activeEditor && event.document === activeEditor.document) {
58 | triggerDecoration();
59 | }
60 | }, null, context.subscriptions));
61 |
62 | if (activeEditor && extname(activeEditor.document.fileName) !== '.tf') {
63 | triggerDecoration();
64 | };
65 | };
66 |
--------------------------------------------------------------------------------
/src/ignore.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { CheckManager } from './check_manager';
3 | import { TfsecIssueProvider } from './explorer/issues_treeview';
4 | import { TfsecTreeItem, TfsecTreeItemType } from './explorer/tfsec_treeitem';
5 |
6 | let timeout: NodeJS.Timer | undefined = undefined;
7 | let activeEditor = vscode.window.activeTextEditor;
8 | import * as path from 'path';
9 |
10 | class IgnoreDetails {
11 | public readonly code: string;
12 | public readonly startLine: number;
13 | public readonly endLine: number;
14 | public constructor(code: string, startLine: number, endLine: number) {
15 | this.code = code;
16 | this.startLine = startLine;
17 | this.endLine = endLine;
18 | }
19 | }
20 |
21 | class FileIgnores {
22 | public constructor(public readonly filename: string, public readonly ignores: IgnoreDetails[]) { }
23 | }
24 |
25 | const tfsecIgnoreDecoration = vscode.window.createTextEditorDecorationType({
26 | fontStyle: 'italic',
27 | color: new vscode.ThemeColor("editorGutter.commentRangeForeground"),
28 | after: {
29 | margin: '0 0 0 1em',
30 | textDecoration: 'none',
31 | },
32 | rangeBehavior: vscode.DecorationRangeBehavior.ClosedOpen,
33 | });
34 |
35 | function triggerDecoration() {
36 | const config = vscode.workspace.getConfiguration('tfsec');
37 | if (!config.get('resolveIgnoreCodes', true)) {
38 | return;
39 | }
40 | if (timeout) {
41 | clearTimeout(timeout);
42 | timeout = undefined;
43 | }
44 | timeout = setTimeout(updateTfsecIgnoreDecorators, 500);
45 | }
46 |
47 | function updateTfsecIgnoreDecorators() {
48 | if (!activeEditor) {
49 | return;
50 | }
51 | const regEx = /tfsec:ignore:([A-Z]+?\d{3})/g;
52 | const text = activeEditor.document.getText();
53 | const tfsecIgnores: vscode.DecorationOptions[] = [];
54 |
55 | let match;
56 | while ((match = regEx.exec(text))) {
57 | if (match[1] === undefined || match[0] === undefined) { break; }
58 | const startPos = activeEditor.document.positionAt(match.index);
59 | const endPos = activeEditor.document.positionAt(match.index + match[0].length);
60 | const message = getTfsecDescription(match[1]);
61 | const decoration = { range: new vscode.Range(startPos, endPos), renderOptions: { after: { fontStyle: 'italic', contentText: message, color: new vscode.ThemeColor("editorGutter.commentRangeForeground") } } };
62 | tfsecIgnores.push(decoration);
63 | }
64 | activeEditor.setDecorations(tfsecIgnoreDecoration, tfsecIgnores);
65 | }
66 |
67 | function getTfsecDescription(tfsecCode: string) {
68 | var check = CheckManager.getInstance().get(tfsecCode);
69 | if (check === undefined) {
70 | return "[Uknown tfsec code]";
71 | }
72 | return check;
73 | }
74 |
75 | function rerunIfRequired() {
76 | const config = vscode.workspace.getConfiguration('tfsec');
77 | var reRunOnIgnore = config.get('runOnIgnore', true);
78 | if (reRunOnIgnore) {
79 | setTimeout(() => { vscode.commands.executeCommand("tfsec.run"); }, 1000);
80 | } else {
81 | vscode.window.showInformationMessage("You should refresh the treeview after ignoring");
82 | };
83 | }
84 |
85 | async function addIgnore(filename: string, ignores: IgnoreDetails[], outputChannel: vscode.OutputChannel): Promise {
86 | if (!filename.endsWith(".tf")) {
87 | outputChannel.appendLine(`${filename} is not a tf file`);
88 | return Promise.resolve();
89 | }
90 |
91 | ignores = ignores.sort(function (a: IgnoreDetails, b: IgnoreDetails): number {
92 | if (a.startLine > b.startLine) {
93 | return 1;
94 | } else if (a.startLine < b.startLine) {
95 | return -1;
96 | }
97 | return 0;
98 | });
99 |
100 |
101 | await vscode.window.showTextDocument(vscode.Uri.file(filename)).then(e => {
102 | e.edit(edit => {
103 | for (let index = 0; index < ignores.length; index++) {
104 | let element = ignores[index];
105 |
106 | if (element === undefined) { continue; }
107 | const ignoreCode = `#tfsec:ignore:${element.code}`;
108 | outputChannel.appendLine(`Adding ignore for ${ignoreCode}`);
109 | var ignoreLine: vscode.TextLine | undefined;
110 | var startPos: number | undefined;
111 | if (element.startLine === element.endLine) {
112 | let errorLine = vscode.window.activeTextEditor?.document.lineAt(element.startLine);
113 | if (errorLine !== null && errorLine !== undefined) {
114 | let ignoreLinePos = element.startLine;
115 | ignoreLine = vscode.window.activeTextEditor?.document.lineAt(ignoreLinePos);
116 | startPos = ignoreLine?.text.length;
117 | }
118 | } else {
119 | let ignoreLinePos = element.startLine;
120 | ignoreLine = vscode.window.activeTextEditor?.document.lineAt(ignoreLinePos);
121 | }
122 | if (ignoreLine === undefined || ignoreLine.text.includes(ignoreCode)) {
123 | continue;
124 | }
125 | if (ignoreLine !== undefined && ignoreLine.text !== undefined && ignoreLine.text.includes('tfsec:')) {
126 | edit.insert(new vscode.Position(ignoreLine.lineNumber - 1, 0), `${ignoreCode}\n`);
127 | } else {
128 | if (startPos === undefined) {
129 | startPos = 0;
130 | }
131 | edit.insert(new vscode.Position(ignoreLine.lineNumber - 1, 0), `${ignoreCode}\n`);
132 | }
133 | }
134 | });
135 | e.document.save();
136 | });
137 | };
138 |
139 | const ignoreInstance = (element: TfsecTreeItem, outputChannel: vscode.OutputChannel) => {
140 | const details = [new IgnoreDetails(element.code, element.startLineNumber, element.endLineNumber)];
141 | addIgnore(element.filename, details, outputChannel);
142 |
143 | rerunIfRequired();
144 | };
145 |
146 |
147 | const ingorePath = (element: any) => {
148 |
149 | if (vscode.workspace && vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]) {
150 | const rootpath = vscode.workspace.workspaceFolders[0].uri.fsPath;
151 | const config = vscode.workspace.getConfiguration("tfsec");
152 | let excludedPaths = config.get("excludedPaths");
153 |
154 | var filepath = element.fsPath;
155 | filepath = path.relative(rootpath, filepath);
156 |
157 | excludedPaths?.push(filepath);
158 | excludedPaths = [...new Set(excludedPaths?.map(obj => obj))];
159 |
160 | config.update("excludedPaths", excludedPaths, false);
161 | }
162 | };
163 |
164 | const ignoreAllInstances = async (element: TfsecTreeItem, issueProvider: TfsecIssueProvider, outputChannel: vscode.OutputChannel) => {
165 | outputChannel.show();
166 | outputChannel.appendLine("\nSetting ignores - ");
167 |
168 | var seenIgnores: string[] = [];
169 | var ignoreMap = new Map();
170 |
171 | let severityIgnore = element.treeItemType === TfsecTreeItemType.issueSeverity;
172 | for (let index = 0; index < issueProvider.resultData.length; index++) {
173 | var r = issueProvider.resultData[index];
174 | if (r === undefined) {
175 | continue;
176 | }
177 | let ignores = ignoreMap.get(r.filename);
178 | if (!ignores) {
179 | ignores = [];
180 | }
181 |
182 | if (severityIgnore && r.severity !== element.severity) { continue; }
183 | if (!severityIgnore && r.code !== element.code) { continue; }
184 |
185 | let ingoreKey = `${r.filename}:${r.code}:${r.startLine}:${r.endLine}`;
186 | if (seenIgnores.includes(ingoreKey)) {
187 | continue;
188 | }
189 | seenIgnores.push(ingoreKey);
190 | ignores.push(new IgnoreDetails(r.code, r.startLine, r.endLine));
191 | ignoreMap.set(r.filename, ignores);
192 | }
193 |
194 | var edits: FileIgnores[] = [];
195 | ignoreMap.forEach((ignores: IgnoreDetails[], filename: string) => {
196 | edits.push(new FileIgnores(filename, ignores));
197 | });
198 |
199 | await edits.reduce(
200 | (p, x) =>
201 | p.then(_ => addIgnore(x.filename, x.ignores, outputChannel)),
202 | Promise.resolve()
203 | ).then(() => {
204 | outputChannel.appendLine("Checking if re-run is enabled....");
205 | rerunIfRequired();
206 | });
207 | };
208 |
209 |
210 | export { ignoreAllInstances, ignoreInstance, ingorePath, triggerDecoration, IgnoreDetails, FileIgnores };
--------------------------------------------------------------------------------
/src/snippets/custom_checks.json:
--------------------------------------------------------------------------------
1 | {
2 | "custom_checks_yaml": {
3 | "prefix": [
4 | "tfsec-check-file"
5 | ],
6 | "body": [
7 | "---",
8 | "checks:",
9 | " - code: ${1}",
10 | " description: ${2}",
11 | " requiredTypes:",
12 | " - ${3:resource}",
13 | " requiredLabels:",
14 | " - ${4}",
15 | " severity: ${5:MEDIUM}",
16 | " matchSpec:",
17 | " name: ${6}",
18 | " action: ${7}",
19 | " value: ${8}",
20 | " errorMessage: ${9}",
21 | " relatedLinks:",
22 | " - ${10}"
23 | ],
24 | "description": "Add the body for custom checks file."
25 | },
26 | "custom_check": {
27 | "prefix": [
28 | "tfsec-custom-check"
29 | ],
30 | "body": [
31 | " - code: ${1}",
32 | " description: ${2}",
33 | " requiredTypes:",
34 | " - ${3:resource}",
35 | " requiredLabels:",
36 | " - ${4}",
37 | " severity: ${5:MEDIUM}",
38 | " matchSpec:",
39 | " name: ${6}",
40 | " action: ${7}",
41 | " value: ${8}",
42 | " errorMessage: ${9}",
43 | " relatedLinks:",
44 | " - ${10}"
45 | ],
46 | "description": "Add the body for an individual custom check."
47 | }
48 | }
--------------------------------------------------------------------------------
/src/tfsec_wrapper.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as child from 'child_process';
3 | import * as semver from 'semver';
4 | import { v4 as uuid } from 'uuid';
5 | import * as path from 'path';
6 | import { unlinkSync, readdirSync } from 'fs';
7 |
8 | export class TfsecWrapper {
9 | private workingPath: string[] = [];
10 | constructor(
11 | private outputChannel: vscode.OutputChannel,
12 | private readonly resultsStoragePath: string) {
13 | if (!vscode.workspace || !vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length <= 0) {
14 | return;
15 | }
16 | const folders = vscode.workspace.workspaceFolders;
17 | for (let i = 0; i < folders.length; i++) {
18 | if (folders[i]) {
19 | const workspaceFolder = folders[i];
20 | if (!workspaceFolder) {
21 | continue;
22 | }
23 | this.workingPath.push(workspaceFolder.uri.fsPath);
24 | }
25 | }
26 | }
27 |
28 | run() {
29 | let outputChannel = this.outputChannel;
30 | this.outputChannel.appendLine("");
31 | this.outputChannel.appendLine("Running tfsec to update results");
32 |
33 | if (!this.checkTfsecInstalled()) {
34 | return;
35 | }
36 |
37 | var files = readdirSync(this.resultsStoragePath).filter(fn => fn.endsWith('_results.json') || fn.endsWith('_results.json.json'));
38 | files.forEach(file => {
39 | let deletePath = path.join(this.resultsStoragePath, file);
40 | unlinkSync(deletePath);
41 | });
42 |
43 | const binary = this.getBinaryPath();
44 |
45 | this.workingPath.forEach(workingPath => {
46 | let command = this.buildCommand(workingPath);
47 | this.outputChannel.appendLine(`command: ${command}`);
48 |
49 | var execution = child.spawn(binary, command);
50 |
51 | execution.stdout.on('data', function (data) {
52 | outputChannel.appendLine(data.toString());
53 | });
54 |
55 | execution.stderr.on('data', function (data) {
56 | outputChannel.appendLine(data.toString());
57 | });
58 |
59 | execution.on('exit', function (code) {
60 | if (code !== 0) {
61 | vscode.window.showErrorMessage("tfsec failed to run");
62 | outputChannel.show();
63 | return;
64 | };
65 | vscode.window.showInformationMessage('tfsec ran successfully, updating results');
66 | outputChannel.appendLine('Reloading the Findings Explorer content');
67 | setTimeout(() => { vscode.commands.executeCommand("tfsec.refresh"); }, 250);
68 | });
69 | });
70 |
71 | }
72 |
73 |
74 | updateBinary() {
75 | this.outputChannel.show();
76 | this.outputChannel.appendLine("");
77 | this.outputChannel.appendLine("Checking the current version");
78 |
79 | if (!this.checkTfsecInstalled()) {
80 | return;
81 | }
82 |
83 | const currentVersion = this.getInstalledTfsecVersion();
84 | if (currentVersion.includes("running a locally built version")) {
85 | this.outputChannel.appendLine("You are using a locally built version which cannot be updated");
86 | }
87 |
88 | if (semver.lt(currentVersion, "0.39.39")) {
89 | vscode.window.showInformationMessage(`Self updating was not introduced till v0.39.39 and you are running ${currentVersion}. Pleae update manually to at least v0.39.39`);
90 | }
91 | this.outputChannel.appendLine("Attempting to download the latest version");
92 | var binary = this.getBinaryPath();
93 | try {
94 | let result: Buffer = child.execSync(binary + " --update --verbose");
95 | this.outputChannel.appendLine(result.toLocaleString());
96 | } catch (err) {
97 | vscode.window.showErrorMessage("There was a problem with the update, check the output window");
98 | let errMsg = err as Error;
99 | this.outputChannel.appendLine(errMsg.message);
100 | }
101 | }
102 |
103 | showCurrentTfsecVersion() {
104 | const currentVersion = this.getInstalledTfsecVersion();
105 | if (currentVersion) {
106 | vscode.window.showInformationMessage(`Current tfsec version is ${currentVersion}`);
107 | }
108 | }
109 |
110 | private getBinaryPath() {
111 | const config = vscode.workspace.getConfiguration('tfsec');
112 | var binary = config.get('binaryPath', 'tfsec');
113 | if (binary === "") {
114 | binary = "tfsec";
115 | }
116 |
117 | return binary;
118 | };
119 |
120 | private checkTfsecInstalled(): boolean {
121 | const binaryPath = this.getBinaryPath();
122 |
123 | var command = [];
124 | command.push(binaryPath);
125 | command.push('--help');
126 | try {
127 | child.execSync(command.join(' '));
128 | }
129 | catch (err) {
130 | this.outputChannel.show();
131 | this.outputChannel.appendLine(`tfsec not found. Check the tfsec extension settings to ensure the path is correct. [${binaryPath}]`);
132 | return false;
133 | }
134 | return true;
135 | };
136 |
137 | private getInstalledTfsecVersion(): string {
138 |
139 | if (!this.checkTfsecInstalled) {
140 | vscode.window.showErrorMessage("tfsec could not be found, check Output window");
141 | return "";
142 | }
143 |
144 | let binary = this.getBinaryPath();
145 |
146 | var command = [];
147 | command.push(binary);
148 | command.push('--version');
149 | const getVersion = child.execSync(command.join(' '));
150 | return getVersion.toString();
151 | };
152 |
153 |
154 | private buildCommand(workingPath: string): string[] {
155 | const config = vscode.workspace.getConfiguration('tfsec');
156 | var command = [];
157 |
158 | if (config.get('fullDepthSearch')) {
159 | command.push('--force-all-dirs');
160 | }
161 | if (config.get('ignoreDownloadedModules')) {
162 | command.push('--exclude-downloaded-modules');
163 | }
164 |
165 | if (config.get('debug')) {
166 | command.push('--verbose');
167 | }
168 |
169 | const excludes = config.get("excludedPaths");
170 | if (excludes && excludes.length > 0) {
171 | excludes.forEach((element: string) => {
172 | command.push(`--exclude-path=${element}`);
173 | });
174 | }
175 |
176 |
177 | // add soft fail for exit code
178 | command.push('--soft-fail');
179 | command.push('--format=json');
180 | const resultsPath = path.join(this.resultsStoragePath, `${uuid()}_results.json`);
181 | command.push(`--out=${resultsPath}`);
182 | command.push(workingPath);
183 |
184 | return command;
185 | }
186 |
187 | }
188 |
189 |
190 |
--------------------------------------------------------------------------------
/tfsec-explorer-usage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquasecurity/vscode-tfsec/55b8d7ae7be501d6c23a77d0519ab7a001543a63/tfsec-explorer-usage.gif
--------------------------------------------------------------------------------
/tfsec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aquasecurity/vscode-tfsec/55b8d7ae7be501d6c23a77d0519ab7a001543a63/tfsec.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "incremental": true,
4 | "target": "es6",
5 | "module": "commonjs",
6 | "sourceMap": true,
7 | "outDir": "./out",
8 | "strict": true,
9 | "noUnusedLocals": true,
10 | "noUnusedParameters": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | // "noUncheckedIndexedAccess": true,
14 | // "noPropertyAccessFromIndexSignature": true,
15 | "esModuleInterop": true,
16 | "skipLibCheck": true,
17 | "forceConsistentCasingInFileNames": true
18 | }
19 | }
--------------------------------------------------------------------------------