├── .eslintrc.json
├── .github
└── workflows
│ ├── package.yml
│ └── release.yml
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── COLLAB_MODE.md
├── README.md
├── images
├── animation-diagram.gif
├── doyensec_logo.png
├── feature-comments.png
├── feature-diagram.png
├── feature-findings.png
├── feature-poi.png
├── logo-1.png
├── supported-lang-csp.png
└── threadExample.png
├── inactive_rules
├── hcl-aws-dynamodb.yaml
├── hcl-aws-s3-bucket.yaml
├── hcl-aws-sns.yaml
└── hcl-aws-sqs.yaml
├── media
├── infrastructure.svg
├── refresh-dark.svg
├── refresh-light.svg
├── remove-dark.svg
└── remove-light.svg
├── package-lock.json
├── package.json
├── res
├── iacView.css
├── iacView.js
├── poiView.css
└── poiView.js
├── rules
├── boto3-put_records.yaml
├── boto3-s3_put_obj.yaml
├── go-aws-generic1.yaml
├── go-awsv1-generic1.yaml
├── js-gcp-generic1.yaml
├── js-generic1.js
├── js-generic1.yaml
├── python-aws-generic1.yaml
├── python-aws-generic2.yaml
├── python-gcp-generic1.yaml
└── ts-generic1.yaml
├── src
├── anchor.ts
├── comments.ts
├── constants.ts
├── db.ts
├── diagnostics.ts
├── encryption.ts
├── extension.ts
├── iacview.ts
├── poiview.ts
├── projects.ts
├── remote.ts
├── semgrep.ts
├── test
│ ├── runTest.ts
│ └── suite
│ │ ├── extension.test.ts
│ │ └── index.ts
├── tree.ts
└── util.ts
├── tfparse_rules
└── hcl-resource.yaml
└── 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 | "out",
21 | "dist",
22 | "**/*.d.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.github/workflows/package.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | workflow_dispatch:
6 |
7 | name: Build VSIX file
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 | - name: Install Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: 16.x
19 | - run: npm install
20 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_arch=x64 --target_libc=unknown
21 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_arch=arm64 --target_libc=unknown
22 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_arch=ia32 --target_libc=unknown
23 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_arch=x64 --target_libc=unknown
24 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=x64 --target_libc=musl
25 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=arm64 --target_libc=musl
26 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=x64 --target_libc=glibc
27 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=arm64 --target_libc=glibc
28 | - run: xvfb-run -a ./node_modules/.bin/vsce package -o "poiex.vsix"
29 | - name: Archive VSIX file
30 | uses: actions/upload-artifact@v3
31 | with:
32 | name: poiex.vsix
33 | path: poiex.vsix
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | tags:
4 | - 'v*'
5 | workflow_dispatch:
6 |
7 | name: Create Release
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v3
15 | - name: Install Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: 16.x
19 | - run: npm install
20 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_arch=x64 --target_libc=unknown
21 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=darwin --target_arch=arm64 --target_libc=unknown
22 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_arch=ia32 --target_libc=unknown
23 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=win32 --target_arch=x64 --target_libc=unknown
24 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=x64 --target_libc=musl
25 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=arm64 --target_libc=musl
26 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=x64 --target_libc=glibc
27 | - run: ./node_modules/.bin/node-pre-gyp install --directory=./node_modules/sqlite3 --target_platform=linux --target_arch=arm64 --target_libc=glibc
28 | - run: xvfb-run -a ./node_modules/.bin/vsce package -o "poiex.vsix"
29 | - name: Create Release
30 | id: create_release
31 | uses: actions/create-release@v1
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | with:
35 | tag_name: ${{ github.ref }}
36 | release_name: Release ${{ github.ref }}
37 | draft: false
38 | prerelease: false
39 | - name: Upload Release Asset
40 | id: upload-release-asset
41 | uses: actions/upload-release-asset@v1
42 | env:
43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 | with:
45 | upload_url: ${{ steps.create_release.outputs.upload_url }}
46 | asset_path: ./poiex.vsix
47 | asset_name: poiex.vsix
48 | asset_content_type: application/octet-stream
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Run Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | },
20 | {
21 | "name": "Extension Tests",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "args": [
25 | "--extensionDevelopmentPath=${workspaceFolder}",
26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
27 | ],
28 | "outFiles": [
29 | "${workspaceFolder}/out/test/**/*.js"
30 | ],
31 | "preLaunchTask": "${defaultBuildTask}"
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false // set this to true to hide the "out" folder with the compiled JS files
5 | },
6 | "search.exclude": {
7 | "out": true // set this to false to include "out" folder in search results
8 | },
9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
10 | "typescript.tsc.autoDetect": "off"
11 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | src/**
4 | .gitignore
5 | .yarnrc
6 | vsc-extension-quickstart.md
7 | **/tsconfig.json
8 | **/.eslintrc.json
9 | **/*.map
10 | **/*.ts
11 |
--------------------------------------------------------------------------------
/COLLAB_MODE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PoiEx
7 |
8 |
9 |
10 | ## Collaboration Mode
11 |
12 | PoiEx allows for real-time synchronization of findings and comments with other users. This mode requires a MongoDB instance shared across all collaborators. See the MongoDB section below for how to deploy a MongoDB instance.
13 |
14 | Once you have a shared MongoDB instance ready, set your name in _Settings > Extensions > PoiEx > Author Name_ and the database URI, which should be the same across all collaborators.
15 | To create a project, the Project Manager should open the desired codebase in VS Code, then click _Init project_ in the PoiEx tab. If the project is encrypted, the automatically generated secret should be sent via a secure channel to all collaborators.
16 | To open an existing project, a collaborator should:
17 | - Ensure PoiEx is connected to the same MongoDB instance as the project manager
18 | - Ensure that in the PoiEx extension settings, the MongoDB database name specified is the same as the project manager
19 | - Open a VS Code workspace with the same codebase as the project manager (the codebase is never uploaded to MongoDB and needs to be shared separately)
20 | - Click _Open existing project_ in the PoiEx tab
21 | - Select the project based on project name and project UUID
22 | - Enter the project secret, as received by the project manager
23 |
24 | After this, all findings and notes will be synchronized in real-time across all collaborators.
25 |
26 | ### Shared MongoDB Instance
27 |
28 | To enable collaboration features, all collaborators should connect to a common MongoDB instance.
29 | All collaborators should have read and write access to the database configured in the `poiex.collab.database` field of the VSCode settings. To enable collaboration features, set `poiex.collab.enabled` to `true` and `poiex.collab.uri` to the MongoDB URI.
30 | Optionally, update `poiex.collab.database` if using a database name different from the default value. If credentials are required to connect to the database, the extension will prompt the user for credentials.
31 | The extension supports an auto-delete feature, if `poiex.collab.expireAfter` is set to a value higher than `0`, it will configure MongoDB to automatically delete projects that are not accessed for the specified number of seconds. The project expiration value is reset each time one of the collaborators accesses the project. The expiration value does not affect project data that is saved locally.
32 | If a local project is not found on the remote database, the extension will push the local version to the remote database.
33 |
34 | Example MongoDB deployment steps on Ubuntu 22.04:
35 |
36 | ```bash
37 | export ADMIN_USERNAME="username"
38 | export ADMIN_PASSWORD="$(openssl rand -base64 12)"
39 | export FQDN="$(hostname)"
40 | echo "Admin password is: $ADMIN_PASSWORD"
41 |
42 | # Install MongoDB from the official repository
43 | curl -fsSL https://pgp.mongodb.com/server-6.0.asc | \
44 | sudo gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg \
45 | --dearmor
46 | echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
47 | apt update
48 | apt install -y mongodb-org
49 |
50 | # Create a new user and enable authentication
51 | systemctl enable mongod
52 | systemctl start mongod
53 | mongosh <<< "admin = db.getSiblingDB(\"admin\"); admin.createUser({ user: \"$ADMIN_USERNAME\", pwd: \"$ADMIN_PASSWORD\", roles: [ { role: \"root\", db: \"admin\" } ]});"
54 | systemctl stop mongod
55 | echo "security:" >> /etc/mongod.conf
56 | echo ' keyFile: "/etc/mongodb_keyfile"' >> /etc/mongod.conf
57 | echo " authorization: enabled" >> /etc/mongod.conf
58 | openssl rand -base64 756 > /etc/mongodb_keyfile
59 | chmod 400 /etc/mongodb_keyfile
60 | chown mongodb:mongodb /etc/mongodb_keyfile
61 |
62 | # Configure a replica set, we need this as the extension relies on changestreams
63 | echo "replication:" >> /etc/mongod.conf
64 | echo ' replSetName: "rs0"' >> /etc/mongod.conf
65 | sed -i "s/127.0.0.1/0.0.0.0/g" /etc/mongod.conf
66 | systemctl start mongod
67 | mongosh -u "$ADMIN_USERNAME" -p "$ADMIN_PASSWORD" --authenticationDatabase "admin" <<< "rs.initiate()"
68 | mongosh -u "$ADMIN_USERNAME" -p "$ADMIN_PASSWORD" --authenticationDatabase "admin" <<< "var x = rs.conf(); x.members[0].host = \"$FQDN:27017\"; rs.reconfig(x);"
69 | ```
70 |
71 | **Security Note**: *The given deployment script is intended for plug&play purposes to test the extension and its collaboration capabilities. For production-safe usages, configure a hardened MongoDB instance machine to fit your needs by following the best practices (see the [documentation](https://www.mongodb.com/docs/manual/administration/security-checklist/))*
72 |
73 | After deployment create additional user(s) for the extension collaborators. Each user should have read/write access to one common database. Each collaborator should enter the same MongoDB URI and database name in the extension settings.
74 |
75 | ### Security Model
76 |
77 | Since the tool is intended for internal usage, currently the MongoDB users (testers) are required to have read and write permissions on the configured database.
78 | Consequently, everyone in the team can list, add or destroy projects.
79 | PoiEx stores the comment content and anchor as encrypted JSON objects as per the RFC7516 specification, using the AES-128-CBC algorithm.
80 | The key is never stored on the remote database and needs to be manually shared by the user to all collaborators via a safe channel.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PoiEx
2 |
3 | ## What's PoiEx?
4 |
5 | **PoiEx** is an experimental VS Code extension built to identify and visualize the Points of Intersection between a web application and the underlying infrastructure.
6 |
7 | Since it was designed to simplify manual code review activities, it was also packed with: [Semgrep](https://semgrep.dev/) support, notes taking and collaboration capabilities.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Read the launch post on the [Doyensec blog](https://blog.doyensec.com/2024/01/30/poiex-release.html) for practical examples and tips.
16 |
17 | ## Try It Out!
18 |
19 | Download and install the VSIX extension from GitHub Releases. Make sure you have [Semgrep](https://semgrep.dev/) installed before running PoiEx.
20 |
21 | ## Points of Intersection Explorer
22 |
23 | A Point of Intersection (PoI) marks where your code interacts with its underlying infrastructure, revealing connections between the implemented logic and the Infrastructure as Code (IaC). PoiEx identifies and visualizes PoIs, allowing testers and cloud security specialists to better understand and identify security vulnerabilities in your cloud applications.
24 | The extension relies on [Inframap](https://github.com/cycloidio/inframap/) to generate an interactive IaC diagram. Each entity in the diagram is clickable to reveal the PoIs, Semgrep results and the IaC definition, linked to the selected entity. By then clicking on a PoI, the user can jump to the relevant code section.
25 |
26 | Below an example infrastructure diagram generation and PoIs exploration.
27 | 
28 |
29 | **Note**: If you do not have a Terraform IaC definition file but you have access to the live infrastructure, you can use reverse-terraforming tools such as [terraformer](https://github.com/GoogleCloudPlatform/terraformer) to generate an IaC file from existing infrastructure.
30 |
31 | ### Core Features
32 |
33 | PoiEx allows users to:
34 |
35 | - Scan the application's code and the IaC definition at the same time with [Semgrep](https://semgrep.dev/), generating explorable results in a user-friendly view inside VS Code's Problems section
36 |
37 | - Visualize the infrastructure diagram generated with [Inframap](https://github.com/cycloidio/inframap/). Additionally, the diagram is enhanced to be interactive, each entity in the diagram is clickable and reveals the enumerated PoIs that are linked to the selected entity. By then clicking on a PoI, it jumps to the relevant code section to review the code in which it is embedded.
38 |
39 | - Take notes using the VS Code Comments API, allowing it to be used also as a standalone code-analysis tool
40 |
41 | - Collaborate with other reviewers on encrypted projects pushed to a [MongoDB](https://www.mongodb.com/) instance
42 |
43 | ### Supported CSPs & Languages
44 |
45 | A custom set of Semgrep rules has been developed. Each pair of language and Cloud Service Provider (CSP) needs a different ruleset, as a rule is generally able to match only one language and one CSP.
46 |
47 | The table below summarizes the currently supported CSPs and languages:
48 |
49 | | Language/CSP | GCP | AWS |
50 | |-----------------|-----|-----|
51 | | Python | Yes | Yes |
52 | | JavaScript | Yes | Yes |
53 | | Go | No | Yes |
54 | | TypeScript | No | Yes |
55 |
56 |
57 | ### Enhancing Reviews with PoiEx
58 | Non-IaC related features were added to support manual reviews in different ways. Such functionalities are unrelated to the cloud infrastructure analysis and they are usable in any code review activity.
59 | A few examples are attached below. Please refer to the [launch blogpost](https://blog.doyensec.com/2024/01/25/poiex-release.html) for detailed use cases.
60 | #### 1. Standalone Semgrep Integration
61 | Scan the application's code and the IaC definition at the same time with [Semgrep](https://semgrep.dev/), generating explorable results in a user-friendly view, inside VS Code's Problems section. The Semgrep ruleset is fully customizable via direct arguments in the Settings page.
62 |
63 | It is also possible to flag the findings as `false positive`,`hot` or `resolved` and have them synced in collaboration mode.
64 | 
65 |
66 | #### 2. Notes Taking
67 | It is possible to start comment threads directly within the codebase for adding details and reactions.
68 |
69 | 
70 |
71 | When collaboration mode is disabled, each project is stored in a local SQLite database. In this mode, projects are not synchronized or shared across different collaborators.
72 |
73 | #### 3. Collaboration with Peers
74 | PoiEx allows for real-time synchronization of findings and comments with other users. This mode requires a MongoDB instance shared across all collaborators. Read more in the [collaboration guide](./COLLAB_MODE.md).
75 | ## Extension Settings
76 |
77 | * `poiex.enableIaC`: Enable IaC features of this extension
78 | * `poiex.authorName`: Author name for comments
79 | * `poiex.semgrepArgs`: Semgrep command line arguments
80 | * `poiex.semgrepTimeout`: Semgrep execution timeout (in seconds)
81 | * `poiex.collab.enabled`: Enable collaboration via MongoDB
82 | * `poiex.collab.uri`: URI of the remote MongoDB server
83 | * `poiex.collab.database`: Name of the MongoDB database
84 | * `poiex.collab.expireAfter`: Auto-delete comments on the remote database after a certain amount of seconds (set to 0 to disable)
85 |
86 | ## Credits
87 |
88 | This project was made with love on the [Doyensec Research Island](https://doyensec.com/research.html) by [Michele Lizzit](https://www.linkedin.com/in/michelelizzit/) for his master's thesis at ETH Zurich, under the mentoring of [Francesco Lacerenza](https://twitter.com/lacerenza_fra).
89 |
90 | We took inspiration from [vscode-security-notes](https://github.com/RefactorSecurity/vscode-security-notes) by RefactorSecurity.
91 |
92 | Download the latest [release](https://github.com/doyensec/PoiEx/releases) and contribute with a star, [bug reports or suggestions](https://github.com/doyensec/PoiEx/issues).
93 |
94 |
95 |
--------------------------------------------------------------------------------
/images/animation-diagram.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/animation-diagram.gif
--------------------------------------------------------------------------------
/images/doyensec_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/doyensec_logo.png
--------------------------------------------------------------------------------
/images/feature-comments.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/feature-comments.png
--------------------------------------------------------------------------------
/images/feature-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/feature-diagram.png
--------------------------------------------------------------------------------
/images/feature-findings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/feature-findings.png
--------------------------------------------------------------------------------
/images/feature-poi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/feature-poi.png
--------------------------------------------------------------------------------
/images/logo-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/logo-1.png
--------------------------------------------------------------------------------
/images/supported-lang-csp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/supported-lang-csp.png
--------------------------------------------------------------------------------
/images/threadExample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/doyensec/PoiEx/0c87db09698afd0f58707d76d1b2844e75924aa3/images/threadExample.png
--------------------------------------------------------------------------------
/inactive_rules/hcl-aws-dynamodb.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: hcl-aws-dynamodb
3 | patterns:
4 | - pattern: |
5 | resource "aws_dynamodb_table" $B {
6 | ...
7 | }
8 | message: "IaC Point Of Intersection: definition of $B AWS DynamoDB"
9 | languages:
10 | - hcl
11 | severity: WARNING
--------------------------------------------------------------------------------
/inactive_rules/hcl-aws-s3-bucket.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: hcl-aws-s3-bucket
3 | patterns:
4 | - pattern: |
5 | resource "aws_s3_bucket_object" $B {
6 | ...
7 | }
8 | message: "IaC Point Of Intersection: definition of $B AWS S3 bucket"
9 | languages:
10 | - hcl
11 | severity: WARNING
--------------------------------------------------------------------------------
/inactive_rules/hcl-aws-sns.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: hcl-aws-sns
3 | patterns:
4 | - pattern: |
5 | resource "aws_sns_topic" $B {
6 | ...
7 | }
8 | message: "IaC Point Of Intersection: definition of $B AWS SNS topic"
9 | languages:
10 | - hcl
11 | severity: WARNING
--------------------------------------------------------------------------------
/inactive_rules/hcl-aws-sqs.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: hcl-aws-sqs
3 | patterns:
4 | - pattern: |
5 | resource "aws_sqs_queue" $B {
6 | ...
7 | }
8 | message: "IaC Point Of Intersection: definition of $B AWS SQS queue"
9 | languages:
10 | - hcl
11 | severity: WARNING
--------------------------------------------------------------------------------
/media/infrastructure.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/refresh-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/refresh-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/remove-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/media/remove-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poiex",
3 | "displayName": "PoiEx",
4 | "description": "Point of Intersection Explorer",
5 | "version": "1.0.0",
6 | "publisher": "doyensec",
7 | "contributors": [
8 | {
9 | "name": "Michele Lizzit",
10 | "email": "michele@lizzit.it",
11 | "url": "https://lizzit.it/"
12 | },
13 | {
14 | "name": "Francesco Lacerenza",
15 | "email": "francesco@doyensec.com",
16 | "url": "https://doyensec.com/"
17 | }
18 | ],
19 | "engines": {
20 | "vscode": "^1.85.0"
21 | },
22 | "categories": [
23 | "Other"
24 | ],
25 | "activationEvents": [
26 | "onStartupFinished",
27 | "onView:poiex"
28 | ],
29 | "extensionDependencies": [
30 | "mindaro-dev.file-downloader"
31 | ],
32 | "main": "./out/extension.js",
33 | "contributes": {
34 | "commands": [
35 | {
36 | "command": "poiex.readSemgrepJson",
37 | "title": "Read Semgrep Json"
38 | },
39 | {
40 | "command": "poiex.create_note",
41 | "title": "Create new comment",
42 | "enablement": "!commentIsEmpty"
43 | },
44 | {
45 | "command": "poiex.replyNote",
46 | "title": "Reply",
47 | "enablement": "!commentIsEmpty"
48 | },
49 | {
50 | "command": "poiex.editNote",
51 | "title": "Edit"
52 | },
53 | {
54 | "command": "poiex.deleteNote",
55 | "title": "Delete"
56 | },
57 | {
58 | "command": "poiex.deleteNoteComment",
59 | "title": "Delete"
60 | },
61 | {
62 | "command": "poiex.saveNote",
63 | "title": "Save"
64 | },
65 | {
66 | "command": "poiex.cancelsaveNote",
67 | "title": "Cancel"
68 | },
69 | {
70 | "command": "poiex.dispose",
71 | "title": "Remove All Notes (DANGER)"
72 | },
73 | {
74 | "command": "iacAudit.refreshProjectTree",
75 | "title": "Refresh",
76 | "icon": {
77 | "light": "media/refresh-dark.svg",
78 | "dark": "media/refresh-light.svg"
79 | }
80 | },
81 | {
82 | "command": "iacAudit.deleteTreeProject",
83 | "title": "Delete project",
84 | "icon": {
85 | "light": "media/remove-dark.svg",
86 | "dark": "media/remove-light.svg"
87 | }
88 | }
89 | ],
90 | "configuration": {
91 | "title": "PoiEx",
92 | "properties": {
93 | "poiex.authorName": {
94 | "type": "string",
95 | "description": "Author name for comments.",
96 | "default": "No-name"
97 | },
98 | "poiex.semgrepArgs": {
99 | "type": "string",
100 | "description": "Semgrep command line arguments.",
101 | "default": "--config auto"
102 | },
103 | "poiex.semgrepTimeout": {
104 | "type": "number",
105 | "description": "Semgrep execution timeout in seconds.",
106 | "default": 240,
107 | "minimum": 10,
108 | "maximum": 7200
109 | },
110 | "poiex.enableIaC": {
111 | "type": "boolean",
112 | "description": "Enable IaC-specific features.",
113 | "default": false
114 | },
115 | "poiex.collab.enabled": {
116 | "type": "boolean",
117 | "description": "Enable collaboration via MongoDB.",
118 | "default": false
119 | },
120 | "poiex.collab.uri": {
121 | "type": "string",
122 | "description": "URI of the remote MongoDB server.",
123 | "default": "mongodb://localhost:27017/"
124 | },
125 | "poiex.collab.database": {
126 | "type": "string",
127 | "description": "Name of the MongoDB database.",
128 | "default": "poiex"
129 | },
130 | "poiex.collab.expireAfter": {
131 | "type": "string",
132 | "description": "Auto-delete comments on remote database after a certain amount of seconds. (Set to 0 to disable)",
133 | "default": "0"
134 | }
135 | }
136 | },
137 | "menus": {
138 | "comments/commentThread/title": [
139 | {
140 | "command": "poiex.deleteNote",
141 | "group": "navigation",
142 | "when": "commentController == poiex && !commentThreadIsEmpty"
143 | }
144 | ],
145 | "comments/commentThread/context": [
146 | {
147 | "command": "poiex.create_note",
148 | "group": "inline",
149 | "when": "commentController == poiex && commentThreadIsEmpty"
150 | },
151 | {
152 | "command": "poiex.replyNote",
153 | "group": "inline",
154 | "when": "commentController == poiex && !commentThreadIsEmpty"
155 | }
156 | ],
157 | "comments/comment/title": [
158 | {
159 | "command": "poiex.editNote",
160 | "group": "group@1",
161 | "when": "commentController == poiex"
162 | },
163 | {
164 | "command": "poiex.deleteNoteComment",
165 | "group": "group@2",
166 | "when": "commentController == poiex && comment == canDelete"
167 | }
168 | ],
169 | "comments/comment/context": [
170 | {
171 | "command": "poiex.cancelsaveNote",
172 | "group": "inline@1",
173 | "when": "commentController == poiex"
174 | },
175 | {
176 | "command": "poiex.saveNote",
177 | "group": "inline@2",
178 | "when": "commentController == poiex"
179 | }
180 | ],
181 | "view/title": [
182 | {
183 | "command": "iacAudit.refreshProjectTree",
184 | "when": "view == iacAudit && workspaceFolderCount > 0 && iacAudit.isProjectOpen == false",
185 | "group": "navigation"
186 | }
187 | ],
188 | "view/item/context": [
189 | {
190 | "command": "iacAudit.deleteTreeProject",
191 | "when": "view == iacAudit && workspaceFolderCount > 0 && iacAudit.isProjectOpen == false",
192 | "group": "inline"
193 | }
194 | ]
195 | },
196 | "viewsContainers": {
197 | "activitybar": [
198 | {
199 | "id": "poiex",
200 | "title": "PoiEx",
201 | "icon": "media/infrastructure.svg"
202 | }
203 | ]
204 | },
205 | "views": {
206 | "poiex": [
207 | {
208 | "id": "iacAudit",
209 | "name": "PoiEx - Semgrep",
210 | "contextualTitle": "Explore Semgrep Findings"
211 | },
212 | {
213 | "id": "iacProjectManager",
214 | "name": "PoiEx - Manage projects",
215 | "contextualTitle": "Manage IaC Audit projects"
216 | }
217 | ]
218 | },
219 | "viewsWelcome": [
220 | {
221 | "view": "iacAudit",
222 | "contents": "Automatically run Semgrep on the currently open workspace folder.\n[Run Semgrep on current Workspace](command:poiex.runSemgrep)\nManually provide a Semgrep JSON file. Make sure that you run Semgrep relative to the currently open workspace directory.\n[Provide Semgrep Json File](command:poiex.readSemgrepJson)\nDelete all findings generated with this extension.\n[Delete all diagnostics](command:poiex.deleteAllDiagnostics)",
223 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == true"
224 | },
225 | {
226 | "view": "iacAudit",
227 | "contents": "Analyze the current project and show IaC-specific diagram.\n[Show IaC diagram](command:poiex.showIaCwebview)",
228 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == true && config.poiex.enableIaC == true"
229 | },
230 | {
231 | "view": "iacAudit",
232 | "contents": "In order to use PoiEx open a workspace folder.\n[Open Folder](command:vscode.openFolder)",
233 | "when": "workspaceFolderCount == 0"
234 | },
235 | {
236 | "view": "iacAudit",
237 | "contents": "Please open a project to use this extension.",
238 | "when": "workspaceFolderCount > 0 && iacAudit.isProjectOpen == false"
239 | },
240 | {
241 | "view": "iacAudit",
242 | "contents": "This extension only works when a single workspace folder is open. Please close all other workspace folders.",
243 | "when": "workspaceFolderCount > 1"
244 | },
245 | {
246 | "view": "iacProjectManager",
247 | "contents": "In order to use PoiEx open a workspace folder.\n[Open Folder](command:vscode.openFolder)",
248 | "when": "workspaceFolderCount == 0"
249 | },
250 | {
251 | "view": "iacProjectManager",
252 | "contents": "This extension only works when a single workspace folder is open. Please close all other workspace folders.",
253 | "when": "workspaceFolderCount > 1"
254 | },
255 | {
256 | "view": "iacProjectManager",
257 | "contents": "Initialize a new project on current Workspace.\n[Init project](command:poiex.initProject)\nOpen existing project from remote database.\n[Open existing project](command:poiex.openProject)",
258 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == false"
259 | },
260 | {
261 | "view": "iacProjectManager",
262 | "contents": "Close current project.\n[Close project](command:poiex.closeProject)\nExport currently open project to a file.\n[Export project](command:poiex.exportProject)\nDestroy current project on local database only.\n[Destroy local project](command:poiex.destroyLocalProject)",
263 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == true"
264 | },
265 | {
266 | "view": "iacProjectManager",
267 | "contents": "Destroy current project on local and remote database.\n[Destroy project](command:poiex.destroyProject)",
268 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == true && iacAudit.isProjectCreator == true"
269 | },
270 | {
271 | "view": "iacProjectManager",
272 | "contents": "Copy encryption key to clipboard.\n[Copy key to clipboard](command:poiex.copyKey)",
273 | "when": "workspaceFolderCount == 1 && iacAudit.isProjectOpen == true && iacAudit.isProjectEncrypted == true"
274 | }
275 | ]
276 | },
277 | "scripts": {
278 | "vscode:prepublish": "npm run compile",
279 | "compile": "tsc -p ./",
280 | "watch": "tsc -watch -p ./",
281 | "pretest": "npm run compile && npm run lint",
282 | "lint": "eslint src --ext ts",
283 | "test": "node ./out/test/runTest.js"
284 | },
285 | "devDependencies": {
286 | "@types/glob": "^8.1.0",
287 | "@types/mocha": "^10.0.6",
288 | "@types/node": "^16.18.70",
289 | "@types/vscode": "^1.85.0",
290 | "@typescript-eslint/eslint-plugin": "^5.62.0",
291 | "@typescript-eslint/parser": "^5.62.0",
292 | "@vscode/test-electron": "^2.3.8",
293 | "@vscode/vsce": "^2.22.0",
294 | "electron": "^26.6.4",
295 | "electron-rebuild": "^3.2.9",
296 | "eslint": "^8.56.0",
297 | "glob": "^8.1.0",
298 | "mocha": "^10.2.0",
299 | "typescript": "^4.9.5"
300 | },
301 | "dependencies": {
302 | "@microsoft/vscode-file-downloader-api": "^1.0.1",
303 | "@types/sqlite3": "^3.1.11",
304 | "@types/ssri": "^7.1.5",
305 | "@types/tar": "^6.1.10",
306 | "@types/url-parse": "^1.4.11",
307 | "@types/which": "^3.0.3",
308 | "jose": "^4.15.4",
309 | "mongodb": "^5.9.2",
310 | "sqlite": "^4.2.1",
311 | "sqlite3": "=5.1.6",
312 | "ssri": "^10.0.5",
313 | "tar": "^6.2.0",
314 | "url-parse": "^1.5.10",
315 | "which": "^3.0.1"
316 | },
317 | "repository": {
318 | "type": "git",
319 | "url": "https://github.com/doyensec/poiex.git"
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/res/iacView.css:
--------------------------------------------------------------------------------
1 | html, body, #diagram {
2 | width: 100%;
3 | height: 100%;
4 | font-family: 'Roboto', sans-serif;
5 | }
6 | #diagram {
7 | border: 1px solid lightgray;
8 | background-color: white;
9 | }
10 |
11 | @font-face {
12 | font-family: 'Roboto';
13 | font-style: normal;
14 | font-weight: 400;
15 | font-display: swap;
16 | src: url(https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2) format('woff2');
17 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
18 | }
--------------------------------------------------------------------------------
/res/iacView.js:
--------------------------------------------------------------------------------
1 | var vscode = acquireVsCodeApi();
2 |
3 | var parsedData = vis.parseDOTNetwork(DOTstring);
4 |
5 | var data = {
6 | nodes: parsedData.nodes,
7 | edges: parsedData.edges
8 | };
9 |
10 | let counterMap = new Map();
11 | for (let i = 0; i < parsedData.nodes.length; i++) {
12 | let node = parsedData.nodes[i].id;
13 | let res = 0;
14 | console.log(node);
15 | for (let poi of poiList) {
16 | let providerFilter = "";
17 | let serviceFilter = "";
18 | let poiFilter = node.split(".")[0];
19 | let poiFilterParts = poiFilter.split("_");
20 | if (poiFilterParts.length >= 1) {
21 | providerFilter = poiFilter.split("_")[0];
22 | }
23 | if (poiFilterParts.length >= 2) {
24 | serviceFilter = poiFilter.split("_")[1];
25 | }
26 |
27 | console.log(poi);
28 | if (poi.toLowerCase().includes(providerFilter) && poi.toLowerCase().includes(serviceFilter)) {
29 | console.log("Found POI for node: " + node);
30 | res++;
31 | }
32 | }
33 |
34 | for (let finding of findingsList) {
35 | if (finding.toLowerCase().includes(node.toLowerCase())) {
36 | res++;
37 | }
38 | }
39 |
40 | counterMap.set(node, res);
41 | }
42 |
43 | // create a network
44 | var container = document.getElementById('diagram');
45 |
46 | var options = {};
47 |
48 | // initialize your network!
49 | var network = new vis.Network(container, data, options);
50 |
51 | network.on("doubleClick", function (params) {
52 | let node = this.getNodeAt(params.pointer.DOM);
53 | vscode.postMessage({
54 | command: 'nodeClicked',
55 | nodeId: node
56 | });
57 | });
58 |
59 | const BUBBLE_X_OFFSET = 20;
60 | const BUBBLE_Y_OFFSET = -20;
61 |
62 | const PALETTE = {
63 | "primary": "#E65100",
64 | "text": "#FAFAFA"
65 | };
66 |
67 | network.on("afterDrawing", function (ctx) {
68 | for (let i = 0; i < parsedData.nodes.length; i++) {
69 | // Draw a bubble with the number of POIs
70 | var nodeId = parsedData.nodes[i].id;
71 | let res = counterMap.get(nodeId);
72 | if (res === undefined) {
73 | res = 0;
74 | }
75 | if (res > 0) {
76 | var nodePosition = network.getPositions([nodeId]);
77 |
78 | ctx.beginPath();
79 | ctx.arc(nodePosition[nodeId].x + BUBBLE_X_OFFSET, nodePosition[nodeId].y + BUBBLE_Y_OFFSET, 13, 0, 2 * Math.PI, false);
80 | ctx.fillStyle = PALETTE.primary;
81 | ctx.fill();
82 |
83 | ctx.font = "15px Roboto";
84 | ctx.fillStyle = PALETTE.text;
85 | ctx.textAlign = "center";
86 | ctx.textBaseline = "middle";
87 | ctx.fillText(res, nodePosition[nodeId].x + BUBBLE_X_OFFSET, nodePosition[nodeId].y + BUBBLE_Y_OFFSET + 1);
88 | }
89 | }
90 | });
--------------------------------------------------------------------------------
/res/poiView.css:
--------------------------------------------------------------------------------
1 | .poitable, .iactable {
2 | width: 100%;
3 | border-collapse: collapse;
4 | border: 1px solid #ddd;
5 | font-size: 13px;
6 | line-height: 1.4em;
7 | --monaco-monospace-font: "Ubuntu Mono","Liberation Mono","DejaVu Sans Mono","Courier New",monospace;
8 | font-family: system-ui,Ubuntu,Droid Sans,sans-serif;
9 | }
10 |
11 | .poitr:hover, .iactr:hover {
12 | background-color: var(--vscode-inputOption-hoverBackground);
13 | }
14 |
15 | .poitd, .poith, .iactd, .iacth {
16 | padding-top: 0.25em;
17 | padding-bottom: 0.25em;
18 | padding-left: 0.5em;
19 | padding-right: 0.5em;
20 | }
21 |
22 | .iactable {
23 | margin-top: 1em;
24 | table-layout: fixed;
25 | }
26 |
27 | pre {
28 | text-wrap: wrap;
29 | }
--------------------------------------------------------------------------------
/res/poiView.js:
--------------------------------------------------------------------------------
1 | var vscode = acquireVsCodeApi();
2 |
3 | function openPoi(poiUUID) {
4 | console.log("[IaC PoiView (webview)] Opening POI: " + poiUUID);
5 | vscode.postMessage({
6 | command: 'openPoi',
7 | poiUUID: poiUUID
8 | });
9 | }
10 |
11 | function openIaC() {
12 | console.log("[IaC PoiView (webview)] Opening IaC definition");
13 | vscode.postMessage({
14 | command: 'openIaC'
15 | });
16 | }
17 |
18 | var container = document.getElementById('container');
19 | let res = `
20 |
21 | Points of Intersection and findings for: ${poiFilter}
22 | `;
23 | // Show the list of POIs
24 | for (let i = 0; i < poiList.length; i++) {
25 | let poiName = poiList[i][0].replace("IaC Point Of Intersection: ", "");
26 | res += `` + poiList[i][1] + ` @ ${poiList[i][2]}:${poiList[i][3]}` + " " + " ";
27 | }
28 | res += "
";
29 |
30 | // Add IaC definition
31 | let iacRes = `
32 |
33 | IaC definition for: ${poiFilter}
34 | `;
35 |
36 | iacRes += `` + iacResource + " " + " ";
37 |
38 | res += iacRes;
39 |
40 | container.innerHTML = res;
--------------------------------------------------------------------------------
/rules/boto3-put_records.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: iac-poi-boto3_put_records
3 | languages:
4 | - python
5 | severity: WARNING
6 | message: "IaC Point Of Intersection: put_records $B"
7 | patterns:
8 | - pattern-either:
9 | - pattern: boto3.client(...).put_records(...)
10 | - pattern: |
11 | $B = boto3.client(...)
12 | $B.put_records(...)
13 |
--------------------------------------------------------------------------------
/rules/boto3-s3_put_obj.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: iac-poi-boto3_s3_put_obj
3 | languages:
4 | - python
5 | severity: WARNING
6 | message: "IaC Point Of Intersection: S3 Bucket put_object"
7 | patterns:
8 | - pattern-either:
9 | - pattern: $W.Bucket(...).put_object(...)
10 | - pattern: |
11 | $B = $W.Bucket(...)
12 | $B.put_object(...)
--------------------------------------------------------------------------------
/rules/js-gcp-generic1.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: js-gcp-generic1
3 | pattern-either:
4 | - patterns:
5 | - pattern-inside: |
6 | $V2 = require("$SN").v1.$V;
7 | ...
8 | - pattern: new $V2(...)
9 | - metavariable-regex:
10 | metavariable: $SN
11 | regex: \@google-cloud/([AZaz\-]*)
12 | - patterns:
13 | - pattern-inside: |
14 | var {$V} = require("$SN").v1;
15 | ...
16 | - pattern: new $V(...)
17 | - metavariable-regex:
18 | metavariable: $SN
19 | regex: \@google-cloud/([AZaz\-]*)
20 | - patterns:
21 | - pattern-inside: |
22 | $V2 = require("$SN").$V;
23 | ...
24 | - pattern: new $V2(...)
25 | - metavariable-regex:
26 | metavariable: $SN
27 | regex: \@google-cloud/([AZaz\-]*)
28 | - patterns:
29 | - pattern-inside: |
30 | var {$V} = require("$SN");
31 | ...
32 | - pattern: new $V(...)
33 | - metavariable-regex:
34 | metavariable: $SN
35 | regex: \@google-cloud/([AZaz\-]*)
36 | message: 'IaC Point Of Intersection: google.cloud.$PN.$SN.$F call to GCP SDK $V'
37 | languages: [javascript]
38 | severity: WARNING
--------------------------------------------------------------------------------
/rules/js-generic1.js:
--------------------------------------------------------------------------------
1 | // import entire SDK
2 | var AWS = require('aws-sdk');
3 | // import AWS object without services
4 | var AWS = require('aws-sdk/global');
5 | // import individual service
6 | var S3 = require('aws-sdk/clients/s3');
7 |
8 | var s3 = new AWS.S3();
9 | s3.abortMultipartUpload(params, function (err, data) {
10 | if (err) console.log(err, err.stack); // an error occurred
11 | else console.log(data); // successful response
12 | });
13 |
14 | var s3 = new S3({apiVersion: '2006-03-01'});
--------------------------------------------------------------------------------
/rules/js-generic1.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: js-generic1
3 | message: 'IaC Point Of Intersection: $SRV call to AWS SDK'
4 | languages:
5 | - javascript
6 | severity: WARNING
7 | pattern-either:
8 | - patterns:
9 | - pattern-inside: |
10 | ... $AWS = require('aws-sdk');
11 | ...
12 | - pattern: new $AWS.$SRV(...)
13 | - patterns:
14 | - pattern-inside: |
15 | ... $AWS = require('aws-sdk');
16 | ...
17 | - pattern: new $AWS.$SRV(...)
18 | - patterns:
19 | - pattern-inside: |
20 | ... $SRV = require("$SN");
21 | ...
22 | - pattern: new $SRV(...)
23 | - metavariable-regex:
24 | metavariable: "$SN"
25 | regex: "^aws-sdk/clients/.*"
--------------------------------------------------------------------------------
/rules/python-aws-generic1.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: python-aws-generic1
3 | languages:
4 | - python
5 | severity: WARNING
6 | message: "IaC Point Of Intersection: $F call to AWS SDK; SN = $SN"
7 | patterns:
8 | - pattern-either:
9 | - pattern: boto3.client($SN, ...).$F(...)
10 | - pattern: |
11 | $B = boto3.client($SN, ...)
12 | $B.$F(...)
13 | - pattern: boto3.resource($SN, ...).$F(...)
14 | - pattern: |
15 | $B = boto3.resource($SN, ...)
16 | $B.$F(...)
17 | - patterns:
18 | - pattern-inside: |
19 | ...
20 | $B = boto3.client($SN, ...)
21 | ...
22 | - pattern: |
23 | $B.$F(...)
--------------------------------------------------------------------------------
/rules/python-aws-generic2.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: python-aws-generic2
3 | languages:
4 | - python
5 | severity: WARNING
6 | message: "IaC Point Of Intersection: Instantiation of AWS SDK; SN = $SN"
7 | patterns:
8 | - pattern-either:
9 | - pattern: boto3.client($SN, ...)
10 | - pattern: boto3.resource($SN, ...)
--------------------------------------------------------------------------------
/rules/python-gcp-generic1.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: python-gcp-generic1
3 | pattern-either:
4 | - patterns:
5 | - pattern-inside: |
6 | from google.cloud.$PN import $SN
7 | ...
8 | - pattern: $SN.$F(...)
9 | - patterns:
10 | - pattern-inside: |
11 | import google.cloud.$PN.$SN
12 | ...
13 | - pattern: google.cloud.$PN.$SN.$F(...)
14 | message: 'IaC Point Of Intersection: google.cloud.$PN.$SN.$F call to GCP SDK'
15 | languages: [python]
16 | severity: WARNING
--------------------------------------------------------------------------------
/rules/ts-generic1.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: ts-generic1
3 | message: 'IaC Point Of Intersection: $F call to AWS SDK'
4 | languages:
5 | - typescript
6 | severity: WARNING
7 | pattern-either:
8 | - patterns:
9 | - pattern-inside: |
10 | import AWS from 'aws-sdk'
11 | ...
12 | - pattern: new AWS.$F(...)
13 | - patterns:
14 | - pattern-inside: |
15 | import AWS from 'aws-sdk/global'
16 | ...
17 | - pattern: new AWS.$F(...)
18 | - patterns:
19 | - pattern-inside: |
20 | import * as $A from 'aws-sdk'
21 | ...
22 | - pattern: new $A.$F(...)
23 | - patterns:
24 | - pattern-inside: |
25 | import $C from "$SN"
26 | ...
27 | - pattern: new $C.$F(...)
28 | - metavariable-regex:
29 | metavariable: "$SN"
30 | regex: "^aws-sdk/clients/.*"
--------------------------------------------------------------------------------
/src/anchor.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'console';
2 |
3 | // Global constants
4 | export const ANCHOR_LINES = 5;
5 | const MINIMUM_MATCH_RATIO = 0.8;
6 |
7 | // Remove newlines, tabs and spaces from anchor text
8 | const NORMALIZE_CHARS = "\r\n\t";
9 |
10 | function normalizeAnchor(anchor: string) {
11 | // Replace NORMALIZE_CHARS with empty string
12 | return anchor.replace(new RegExp("[" + NORMALIZE_CHARS + "]", "g"), "");
13 | }
14 |
15 | // Rabin fingerprint
16 | // https://en.wikipedia.org/wiki/Rabin_fingerprint
17 | function rabinFingerprint(anchor: string, hashSize: number) {
18 | let p = 53;
19 | let m = 2 ** 20;
20 | let hash = 0;
21 | for (let i = 0; i < hashSize; i++) {
22 | hash = (hash * p + anchor.charCodeAt(i)) % m;
23 | }
24 | return hash;
25 | }
26 |
27 | // Fast modular exponentiation
28 | function modExp(base: number, exp: number, mod: number) {
29 | let result = 1;
30 | while (exp > 0) {
31 | if (exp % 2 === 1) {
32 | result = (result * base) % mod;
33 | }
34 | exp = Math.floor(exp / 2);
35 | base = (base * base) % mod;
36 | }
37 | return result;
38 | }
39 |
40 | // Update the rabin fingerprint of the anchor text
41 | function updateRabinFingerprint(oldHash: number, hashSize: number, oldChar: string, newChar: string) {
42 | let p = 53;
43 | let m = 2 ** 20;
44 | let hash = (oldHash - oldChar.charCodeAt(0) * modExp(p, (hashSize - 1), m)) % m;
45 | hash = (hash * p + newChar.charCodeAt(0)) % m;
46 | hash = (hash + m) % m;
47 | return hash;
48 | }
49 |
50 | // Use the rabin fingerprint to find the closest matching anchor in a larger text
51 | export function findClosestAnchor(anchor: string, text: string) {
52 | if (text.length < anchor.length) {
53 | console.log("Text is smaller than anchor");
54 | return -1;
55 | }
56 | else if (text.length === 0) {
57 | console.log("Text is empty");
58 | return -1;
59 | }
60 | else if (anchor.length <= 8) {
61 | console.log("Anchor is smaller than hash size");
62 | return -1;
63 | }
64 |
65 | let numLinesInAnchor = anchor.split(/\r\n|\r|\n/).length;
66 | anchor = normalizeAnchor(anchor);
67 |
68 | // Build hash set of anchor
69 | let hashSize = 8;
70 | let anchorHashes = new Set();
71 | let currentAnchorSample = anchor.substring(0, hashSize);
72 | let lastHash = rabinFingerprint(currentAnchorSample, hashSize);
73 | let cnt = 0;
74 | for (let i = 0; i <= anchor.length - hashSize; i++) {
75 | cnt += 1;
76 | anchorHashes.add(lastHash);
77 | lastHash = updateRabinFingerprint(lastHash, hashSize, currentAnchorSample[0], anchor[(i + hashSize) % anchor.length]);
78 | currentAnchorSample = currentAnchorSample.substring(1) + anchor[(i + hashSize) % anchor.length];
79 | }
80 | assert(cnt === anchor.length - hashSize + 1);
81 | assert(anchorHashes.size <= anchor.length - hashSize + 1);
82 |
83 | // Build initial hash of text
84 | let textHashes = [];
85 | let start = 0;
86 | let currentLine = 0;
87 | let text2Hash = "";
88 | while (text2Hash.length < hashSize) {
89 | if (!NORMALIZE_CHARS.includes(text[start])) {
90 | text2Hash += text[start];
91 | }
92 | else if (text[start] === "\n") {
93 | currentLine++;
94 | }
95 | start++;
96 | if (start > text.length) {
97 | console.log("Could not find enough text for anchor");
98 | return -1;
99 | }
100 | }
101 | start -= hashSize;
102 |
103 | // Build hash set of text and find best match with anchor hash set
104 | lastHash = rabinFingerprint(text2Hash, hashSize);
105 | textHashes.push(lastHash);
106 | let currentTextSample = text2Hash;
107 | let bestMatch = -1;
108 | let bestMatchScore = 0;
109 | for (let i = start; i <= text.length - hashSize; i++) {
110 | // Skip over characters that we don't care about, but keep track of line numbers
111 | if (NORMALIZE_CHARS.includes(text[(i + hashSize) % text.length])) {
112 | if (text[(i + hashSize) % text.length] === "\n") {
113 | currentLine++;
114 | }
115 | continue;
116 | }
117 |
118 | // Determine how many hashes match
119 | let numMatches = 0;
120 | for (let j = 0; j < textHashes.length; j++) {
121 | if (anchorHashes.has(textHashes[j])) {
122 | numMatches++;
123 | }
124 | }
125 | if (numMatches >= anchorHashes.size * MINIMUM_MATCH_RATIO) {
126 | if (numMatches > bestMatchScore) {
127 | bestMatchScore = numMatches;
128 | bestMatch = currentLine;
129 | }
130 | }
131 |
132 | // Update hash sets
133 | lastHash = updateRabinFingerprint(lastHash, hashSize, currentTextSample[0], text[(i + hashSize) % text.length]);
134 | currentTextSample = currentTextSample.substring(1) + text[(i + hashSize) % text.length];
135 | textHashes.push(lastHash);
136 | if (textHashes.length > anchorHashes.size) {
137 | textHashes.shift();
138 | }
139 | }
140 |
141 | if (bestMatch === -1) {
142 | console.log("Could not find match in text");
143 | return -1;
144 | }
145 | return bestMatch - numLinesInAnchor + 1;
146 | }
147 |
--------------------------------------------------------------------------------
/src/comments.ts:
--------------------------------------------------------------------------------
1 | import { LocalDB } from './db';
2 | import { assert } from 'console';
3 | import * as vscode from 'vscode';
4 | import { ANCHOR_LINES, findClosestAnchor } from './anchor';
5 | import { RemoteDB } from './remote';
6 | import * as util from './util';
7 | import * as constants from './constants';
8 |
9 | let isProjectEncrypted: boolean | undefined = undefined;
10 |
11 | interface OurThread extends vscode.CommentThread {
12 | id: string;
13 | }
14 |
15 | // NoteComment
16 | class NoteComment implements vscode.Comment {
17 | id: string;
18 | label: string | undefined;
19 | savedBody: string | vscode.MarkdownString; // for the Cancel button
20 | lastModified: number;
21 | constructor(
22 | public body: string | vscode.MarkdownString,
23 | public mode: vscode.CommentMode,
24 | public author: vscode.CommentAuthorInformation,
25 | public parent?: vscode.CommentThread,
26 | public contextValue?: string
27 | ) {
28 | if (isProjectEncrypted === false) {
29 | this.label = "(not signed, not encrypted)";
30 | }
31 | else if (isProjectEncrypted === true) {
32 | this.label = "(not signed, encrypted)";
33 | }
34 | else {
35 | this.label = "(not signed, ???)";
36 | }
37 | this.id = util.genUUID();
38 | this.savedBody = this.body;
39 | this.lastModified = Date.now() / 1000;
40 | }
41 | }
42 |
43 | export class IaCComments {
44 | private db: LocalDB;
45 | private rdb: RemoteDB;
46 | private threads: OurThread[] = [];
47 | private threadIdMap: Map = new Map();
48 | private commentController: vscode.CommentController;
49 | private lastRemoteSync: number = 0;
50 | private currentSync: undefined | Promise = undefined;
51 | private disposables: vscode.Disposable[] = [];
52 | private disposed: boolean = false;
53 | private context: vscode.ExtensionContext;
54 | private nextSync: boolean = false;
55 |
56 | constructor(context: vscode.ExtensionContext, db: LocalDB, rdb: RemoteDB) {
57 | this.db = db;
58 | this.rdb = rdb;
59 | this.context = context;
60 |
61 | // Check if project is encrypted, update global
62 | isProjectEncrypted = this.context.workspaceState.get('projectEncrypted', undefined);
63 |
64 | // Create a comment controller
65 | this.commentController = vscode.comments.createCommentController(constants.EXT_NAME, 'IaC Comments');
66 | this.commentController.commentingRangeProvider = {
67 | provideCommentingRanges: (document: vscode.TextDocument, token: vscode.CancellationToken) => {
68 | // Allow commenting on all lines
69 | return [new vscode.Range(0, 0, document.lineCount - 1, 0)];
70 | }
71 | };
72 |
73 | context.subscriptions.push(this.commentController);
74 | this.disposables.push(this.commentController);
75 |
76 | // Create comment button, when comment thread is empty
77 | let disposableCommand1 = vscode.commands.registerCommand(`${constants.EXT_NAME}.create_note`, async (reply: vscode.CommentReply) => {
78 | await this.createComment(reply.text, reply.thread);
79 | this.safeSyncComments();
80 | });
81 | context.subscriptions.push(disposableCommand1);
82 | this.disposables.push(disposableCommand1);
83 |
84 | // Reply to comment, when comment thread is not empty
85 | let disposableCommand2 = vscode.commands.registerCommand(`${constants.EXT_NAME}.replyNote`, async (reply: vscode.CommentReply) => {
86 | await this.createComment(reply.text, reply.thread);
87 | this.safeSyncComments();
88 | });
89 | context.subscriptions.push(disposableCommand2);
90 | this.disposables.push(disposableCommand2);
91 |
92 | // Delete comment
93 | let disposableCommand3 = vscode.commands.registerCommand(`${constants.EXT_NAME}.deleteNoteComment`, async (comment: NoteComment) => {
94 | const thread = comment.parent;
95 | if (!thread) { return; }
96 |
97 | await this.deleteComment(thread, comment.id);
98 | this.safeSyncComments();
99 | });
100 | context.subscriptions.push(disposableCommand3);
101 | this.disposables.push(disposableCommand3);
102 |
103 | // Delete comment thread
104 | let disposableCommand4 = vscode.commands.registerCommand(`${constants.EXT_NAME}.deleteNote`, async (thread: vscode.CommentThread) => {
105 | // Delete from global thread list
106 | await this.deleteThread(thread);
107 | thread.dispose();
108 | this.safeSyncComments();
109 | });
110 | context.subscriptions.push(disposableCommand4);
111 | this.disposables.push(disposableCommand4);
112 |
113 | // Cancel editing comment without saving
114 | let disposableCommand5 = vscode.commands.registerCommand(`${constants.EXT_NAME}.cancelsaveNote`, (comment: NoteComment) => {
115 | if (!comment.parent) {
116 | return;
117 | }
118 |
119 | comment.parent.comments = comment.parent.comments.map(cmt => {
120 | if ((cmt as NoteComment).id === comment.id) {
121 | cmt.body = (cmt as NoteComment).savedBody;
122 | cmt.mode = vscode.CommentMode.Preview;
123 | }
124 |
125 | return cmt;
126 | });
127 | });
128 | context.subscriptions.push(disposableCommand5);
129 | this.disposables.push(disposableCommand5);
130 |
131 | // Save edited comment
132 | let disposableCommand6 = vscode.commands.registerCommand(`${constants.EXT_NAME}.saveNote`, async (comment: NoteComment) => {
133 | if (!comment.parent) {
134 | return;
135 | }
136 |
137 | await this.updateComment((comment.parent as OurThread), comment, comment.body.toString());
138 | this.safeSyncComments();
139 | });
140 | context.subscriptions.push(disposableCommand6);
141 | this.disposables.push(disposableCommand6);
142 |
143 | // Edit comment
144 | let disposableCommand7 = vscode.commands.registerCommand(`${constants.EXT_NAME}.editNote`, (comment: NoteComment) => {
145 | if (!comment.parent) {
146 | return;
147 | }
148 |
149 | comment.parent.comments = comment.parent.comments.map(cmt => {
150 | if ((cmt as NoteComment).id === comment.id) {
151 | cmt.mode = vscode.CommentMode.Editing;
152 | }
153 |
154 | return cmt;
155 | });
156 | });
157 | context.subscriptions.push(disposableCommand7);
158 | this.disposables.push(disposableCommand7);
159 |
160 | // Delete all comments
161 | // TODO: add confirmation dialog
162 | let disposableCommand8 = vscode.commands.registerCommand(`${constants.EXT_NAME}.deleteAllNotes`, async () => {
163 | this.threads.forEach(async (thread) => await this.deleteThread(thread));
164 | // Never dispose of the comment controller
165 | // commentController.dispose();
166 | });
167 | context.subscriptions.push(disposableCommand8);
168 | this.disposables.push(disposableCommand8);
169 |
170 | // Load threads from local database
171 | this.initComments().then(() => {
172 | // Sync with remote, now or when db is ready
173 | this.rdb.onDbReady(async () => {
174 | if (this.disposed) { return; }
175 | this.safeSyncComments();
176 | });
177 |
178 | this.rdb.onCommentsUpdated(async () => {
179 | if (this.disposed) { return; }
180 | console.log("[IaC Comments] Comments updated");
181 | this.safeSyncComments();
182 | });
183 | });
184 | }
185 |
186 | // Will check if a sync is currently in progress, and if not, will start one
187 | private safeSyncComments() {
188 | if (this.disposed) { return; }
189 | if (this.currentSync !== undefined) {
190 | this.nextSync = true;
191 | console.log("[IaC Comments] Sync already in progress, syncing later");
192 | return;
193 | }
194 | this.currentSync = this.syncComments().finally(() => {
195 | this.currentSync = undefined;
196 | if (this.nextSync) {
197 | console.log("[IaC Comments] Sync done, doing next sync");
198 | this.nextSync = false;
199 | this.safeSyncComments();
200 | }
201 | else {
202 | console.log("[IaC Comments] Sync done");
203 | }
204 | });
205 | }
206 |
207 | private async createComment(text: string, thread: vscode.CommentThread, author?: string | undefined, id?: string | undefined, lastModified?: number | undefined) {
208 | let commentAuthor = author ? { name: author } : { name: vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).get("authorName", "") };
209 |
210 | const newComment = new NoteComment(
211 | text,
212 | vscode.CommentMode.Preview,
213 | commentAuthor,
214 | thread,
215 | true ? 'canDelete' : undefined,
216 | );
217 | if (id) {
218 | newComment.id = id;
219 | }
220 | if (lastModified) {
221 | newComment.lastModified = lastModified;
222 | }
223 | else {
224 | lastModified = await this.rdb.getRemoteTimestamp();
225 | }
226 | thread.comments = [...thread.comments, newComment];
227 |
228 | // Update global thread list
229 | await this.updateThreadInLocalDb(thread);
230 | }
231 |
232 | private async updateComment(thread: OurThread, comment: NoteComment, text: string, lastModified?: number | undefined) {
233 | comment.body = text;
234 | comment.savedBody = text;
235 | if (lastModified === undefined) {
236 | lastModified = await this.rdb.getRemoteTimestamp();
237 | }
238 |
239 | // Update comment in the parent thread, not local object copy
240 | thread.comments = thread.comments.map(cmt => {
241 | if ((cmt as NoteComment).id !== comment.id) { return cmt; };
242 | (cmt as NoteComment).savedBody = cmt.body;
243 | cmt.body = text;
244 | (cmt as NoteComment).lastModified = (lastModified as number);
245 | return cmt;
246 | });
247 |
248 | // Update global thread list
249 | await this.updateThreadInLocalDb(thread);
250 | }
251 |
252 | async deleteComment(thread: vscode.CommentThread, commentId: string) {
253 | thread.comments = thread.comments.filter(cmt => (cmt as NoteComment).id !== commentId);
254 |
255 | // Update global thread list
256 | await this.updateThreadInLocalDb(thread, false, true);
257 |
258 | if (thread.comments.length === 0) {
259 | // Delete from global thread list
260 | this.deleteThread(thread);
261 |
262 | thread.dispose();
263 | }
264 | }
265 |
266 | private async createThread(id: string, anchorLine: number, filePath: string) {
267 | let commentThread = this.commentController.createCommentThread(
268 | vscode.Uri.file(filePath),
269 | new vscode.Range(anchorLine, 0, anchorLine, 0),
270 | []
271 | ) as OurThread;
272 | commentThread.id = id;
273 | console.log("[IaC Comments] Created thread with id " + id);
274 | await this.updateThreadInLocalDb(commentThread, false);
275 | return commentThread;
276 | }
277 |
278 | private anchor2anchorLine(rawAnchor: string, docdata: string) {
279 | let parsedAnchor = JSON.parse(rawAnchor);
280 | let anchorLineOrig = parsedAnchor["line"];
281 | let anchorText = parsedAnchor["text"];
282 | let numLinesInAnchor = parsedAnchor["num"];
283 |
284 | let anchorLine = findClosestAnchor(anchorText, docdata);
285 | if (anchorLine === -1) {
286 | anchorLine = anchorLineOrig;
287 | }
288 | else {
289 | anchorLine = anchorLine + numLinesInAnchor;
290 | }
291 | console.log("[IaC Comments] Closest anchor: " + anchorLine);
292 | return anchorLine;
293 | };
294 |
295 | private async initComments() {
296 | // Load database content into global thread list
297 | let [dbThreads, dbComments] = await this.db.dbGetThreads();
298 | dbThreads.forEach((row: any) => {
299 | console.log("[IaC Comments] Creating thread at line " + row.anchor + " in file " + row.file_path);
300 | // Create comment thread
301 | //console.log("Anchor has lines: " + anchorText.split(/\r\n|\r|\n/).length);
302 | //console.log("Anchor numLinesInAnchor: " + numLinesInAnchor);
303 | vscode.workspace.fs.readFile(vscode.Uri.file(row.file_path)).then(async (data) => {
304 | let anchorLine = this.anchor2anchorLine(row.anchor, data.toString());
305 |
306 | let commentThread = await this.createThread(row.id, anchorLine, row.file_path);
307 |
308 | // Load database content into comment thread
309 | let commentArr = dbComments.get(row.id);
310 | for (const comment of commentArr) {
311 | console.log("[IaC Comments] Creating comment " + comment.comment + " by " + comment.user_created);
312 | this.createComment(comment.comment, commentThread, comment.user_created, comment.id, comment.timestamp_updated);
313 | }
314 | });
315 | });
316 | }
317 |
318 | async updateThreadInLocalDb(thread: vscode.CommentThread, deleteEmptyThreads: boolean = true, processDeletions: boolean = false) {
319 | // Add thread to global list
320 | console.log("[IaC Comments] Updating thread in local db");
321 | if (!this.threadIdMap.has(thread)) {
322 | let tid = util.genUUID();
323 | let oThread = thread as OurThread;
324 | if (oThread.id) {
325 | tid = oThread.id;
326 | }
327 | console.log("[IaC Comments] Adding thread with id " + tid + " to global list");
328 | oThread.id = tid;
329 | this.threads.push(oThread);
330 | this.threadIdMap.set(thread, tid);
331 | }
332 |
333 | // Update thread in the database
334 | let tid = this.threadIdMap.get(thread);
335 | if (tid === undefined) {
336 | // This should never happen
337 | return;
338 | }
339 |
340 | let oThread = thread as OurThread;
341 | oThread.id = tid;
342 |
343 | let anchor = this.thread2anchor(thread);
344 |
345 | let filePath = oThread.uri.path;
346 | await this.db.createOrReplaceThread(tid, anchor, filePath);
347 |
348 | if (deleteEmptyThreads && thread.comments.length === 0) {
349 | await this.deleteThread(thread);
350 | return;
351 | }
352 |
353 | // Update comments in the database
354 | for (let comment of thread.comments) {
355 | let oComment = comment as NoteComment;
356 | await this.db.dbCreateOrReplaceComment(oComment.id, tid, oComment.body.toString(), oComment.author.name, oComment.lastModified);
357 | console.log("[IaC Comments] Inserting comment " + oComment.body + " by " + oComment.author.name + " into thread " + tid);
358 | }
359 |
360 | if (!processDeletions) {
361 | return;
362 | }
363 |
364 | // Get a list of all comment ids in the thread from the database
365 | let commentIds: string[] = await this.db.getCommentsForThread(tid);
366 | // Filter only comment ids that are not in the thread.
367 | let newCommentIds = commentIds.filter(cid => !thread.comments.map(comment => (comment as NoteComment).id).includes(cid));
368 |
369 | // Delete comments that are not in the thread
370 | if (newCommentIds.length > 0) {
371 | console.log("[IaC Comments] Deleting comments " + newCommentIds + " from thread " + tid + " because they are not in the thread");
372 | await this.db.dbDeleteComments(newCommentIds);
373 | }
374 | }
375 |
376 | private thread2anchor(oThread: vscode.CommentThread) {
377 | // Compute anchor
378 | let curDocument = vscode.workspace.textDocuments.find((doc) => doc.uri.path === oThread.uri.path);
379 | let anchorLine = oThread.range.start.line;
380 | let anchorText = "";
381 | let numLinesFromAnchor = 0;
382 | if (curDocument !== undefined) {
383 | let anchorTextLineBegin = Math.max(anchorLine - ANCHOR_LINES, 0);
384 | let anchorTextLineEnd = Math.min(anchorLine + ANCHOR_LINES, curDocument.lineCount);
385 | let anchorText1 = curDocument.getText(new vscode.Range(anchorTextLineBegin, 0, anchorLine, 0));
386 | let anchorText2 = curDocument.getText(new vscode.Range(anchorLine, 0, anchorTextLineEnd, 0));
387 |
388 | // Limit size of anchor text to 1000 characters
389 | anchorText1 = anchorText1.substring(Math.max(anchorText1.length - 500, 0));
390 | anchorText2 = anchorText2.substring(0, 500);
391 | numLinesFromAnchor = anchorText1.split(/\r\n|\r|\n/).length - 1;
392 | anchorText = anchorText1 + anchorText2;
393 | }
394 | let anchor = JSON.stringify({ "line": anchorLine, "text": anchorText, "num": numLinesFromAnchor });
395 | return anchor;
396 | }
397 |
398 | async deleteThread(thread: vscode.CommentThread) {
399 | // Remove thread from global list
400 | if (!this.threadIdMap.has(thread)) {
401 | return;
402 | }
403 | let tid = this.threadIdMap.get(thread);
404 | if (tid === undefined) {
405 | // This should never happen
406 | assert(false, "[IaC Comments] tid is undefined");
407 | return;
408 | }
409 | this.threads = this.threads.filter(t => t.id !== tid);
410 |
411 | await this.db.dbDeleteTid(tid);
412 | }
413 |
414 | async syncComments() {
415 | let deletedComments = await this.db.dbGetDeletedComments();
416 | let deletedThreads = await this.db.dbGetDeletedThreads();
417 |
418 | console.log("[IaC comments] syncComments();");
419 | if (!this.rdb.isRemoteReady()) {
420 | console.log("[IaC comments] Remote database not ready");
421 | return;
422 | }
423 |
424 | // Fetch comments from remote database
425 | let [remoteObjects, remoteDeletions] = await this.rdb.getComments(this.lastRemoteSync);
426 |
427 | // Push local deletions to remote database
428 | let remoteDeletedComments = remoteDeletions.filter((deletion: any) => deletion.type === "comment").map((deletion: any) => deletion.cid);
429 | let remoteDeletedThreads = remoteDeletions.filter((deletion: any) => deletion.type === "thread").map((deletion: any) => deletion.tid);
430 | let reloadRemote = false;
431 | for (const [cid, tid] of deletedComments) {
432 | if (remoteDeletedComments.includes(cid)) { continue; };
433 | await this.rdb.pushComment(cid, tid, null, null, null, true);
434 | reloadRemote = true;
435 | }
436 | for (const tid of deletedThreads) {
437 | if (remoteDeletedThreads.includes(tid)) { continue; };
438 | await this.rdb.pushThread(tid, null, true, null, null);
439 | reloadRemote = true;
440 | }
441 | if (reloadRemote) {
442 | [remoteObjects, remoteDeletions] = await this.rdb.getComments(this.lastRemoteSync);
443 | }
444 |
445 | // TODO: fix issues in case local clock is not in sync with remote clock
446 | this.lastRemoteSync = await this.rdb.getRemoteTimestamp();
447 | // Temporary fix: sync all comments all the time
448 | this.lastRemoteSync = 0;
449 |
450 | // If there are any deletions, delete them from the local database
451 | for (let deletion of remoteDeletions) {
452 | if (deletion.type === "thread") {
453 | // Get thread
454 | let thread = this.threads.find(t => t.id === deletion.tid);
455 | if (thread !== undefined) {
456 | console.log("[IaC Comments] Remote deletion, deleting thread " + thread.id);
457 | if (this.disposed) { return; }
458 | await this.deleteThread(thread);
459 | }
460 | }
461 | else if (deletion.type === "comment") {
462 | // Get comment
463 | let thread = this.threads.find(t => t.id === deletion.tid);
464 | if (thread !== undefined) {
465 | console.log("[IaC Comments] Remote deletion, deleting comment " + deletion.cid + " from thread " + thread.id);
466 | if (this.disposed) { return; }
467 | await this.deleteComment(thread, deletion.cid);
468 | }
469 | }
470 | }
471 |
472 | // If there is any new or edited comment or new thread, update the local database and the UI
473 | let remoteThreads = remoteObjects.filter((object: any) => object.type === "thread");
474 | let remoteComments = remoteObjects.filter((object: any) => object.type === "comment");
475 |
476 | for (const object of remoteThreads) {
477 | // Get thread
478 | let thread = this.threads.find(t => t.id === object.tid);
479 | if (thread === undefined) {
480 | // Create new thread
481 | const absPath = util.relPathToAbs(object.filePath);
482 | let data = await vscode.workspace.fs.readFile(vscode.Uri.file(absPath));
483 | let anchorLine = this.anchor2anchorLine(object.anchor, data.toString());
484 | if (this.disposed) { return; }
485 | await this.createThread(object.tid, anchorLine, absPath);
486 | console.log("[IaC Comments] List of thread ids after createThread:" + this.threads.map(t => t.id));
487 | }
488 | else {
489 | // Update existing thread
490 | // TODO: update anchor
491 | // Thread id and file path are immutable and cannot be changed
492 | }
493 | }
494 |
495 | for (const object of remoteComments) {
496 | // Get thread
497 | let thread = this.threads.find(t => t.id === object.tid);
498 | if (thread === undefined) {
499 | console.error("[IaC Comments] This should never happen. Comment without thread. Remote database is corrupted.");
500 | console.log("[IaC Comments] List of thread ids:" + this.threads.map(t => t.id));
501 | console.log("[IaC Comments] Comment: " + object.tid);
502 | return;
503 | }
504 |
505 | // Get comment
506 | let comment = thread.comments.find(c => (c as NoteComment).id === object.cid);
507 | if (comment === undefined) {
508 | // Create new comment
509 | if (this.disposed) { return; }
510 | await this.createComment(object.comment, thread, object.userCreated, object.cid, object.timestampModified);
511 |
512 | // Notify user
513 | let relPath = util.absPathToRel(thread.uri.fsPath);
514 |
515 | // Show a notification with a "Jump to comment" button
516 | const showCommentAction = 'Show comment';
517 | vscode.window.showInformationMessage(`[${object.userCreated}] 📥 Added a comment on ${relPath}`, showCommentAction).then(
518 | (selection) => {
519 | if ((selection === showCommentAction) && (thread !== undefined)) {
520 | // Jump to comment
521 | vscode.window.showTextDocument(thread.uri).then((editor) => {
522 | editor.revealRange((thread as OurThread).range, vscode.TextEditorRevealType.InCenter);
523 | });
524 | }
525 | }
526 | );
527 | }
528 | else {
529 | // Update existing comment
530 | console.log("[IaC Comments] Remote comment updated an existing comment");
531 | console.log("[IaC Comments] Comment last modified: " + (comment as NoteComment).lastModified);
532 | console.log("[IaC Comments] Remote last modified: " + object.timestampModified);
533 | if ((comment as NoteComment).lastModified + 1 < object.timestampModified) {
534 | console.log(`[IaC Comments] Updating comment ${object.cid} as it is newer than the local version`);
535 | if (this.disposed) { return; }
536 | await this.updateComment(thread, (comment as NoteComment), object.comment, object.timestampModified);
537 |
538 | // Notify user
539 | let relPath = util.absPathToRel(thread.uri.fsPath);
540 |
541 | // Show a notification with a "Jump to comment" button
542 | const showCommentAction = 'Show comment';
543 | vscode.window.showInformationMessage(`[${object.userCreated}] ✍️ Updated a comment on ${relPath}`, showCommentAction).then(
544 | (selection) => {
545 | if ((selection === showCommentAction) && (thread !== undefined)) {
546 | // Jump to comment
547 | vscode.window.showTextDocument(thread.uri).then((editor) => {
548 | editor.revealRange((thread as OurThread).range, vscode.TextEditorRevealType.InCenter);
549 | });
550 | }
551 | }
552 | );
553 | }
554 | }
555 | }
556 |
557 | // Determine which comments and threads are not in the remote database or have been deleted or edited more recently than the remote database
558 | let localThreads = this.threads.filter((thread: vscode.CommentThread) => {
559 | let oThread = thread as OurThread;
560 | let remoteThread = remoteThreads.find((object: any) => object.tid === oThread.id);
561 | if (remoteThread === undefined) {
562 | return true;
563 | }
564 | // return remoteThread.last_modified < oThread.lastModified;
565 | // TODO: check if anchor has changed and update it
566 | return false;
567 | });
568 |
569 | let localComments = this.threads.map((thread: vscode.CommentThread) => {
570 | return thread.comments.filter((comment: vscode.Comment) => {
571 | let oComment = comment as NoteComment;
572 | let remoteComment = remoteComments.find((object: any) => object.cid === oComment.id);
573 | if (remoteComment === undefined) {
574 | console.log(`[IaC Comments] Local comment ${oComment.id} not found in remote database, will push it`);
575 | return true;
576 | }
577 | console.log(`[IaC Comments] ${remoteComment.timestampModified} < ${oComment.lastModified}`);
578 | if (remoteComment.timestampModified < oComment.lastModified) {
579 | console.log(`[IaC Comments] Local comment ${oComment.id} is newer than remote version, will push it`);
580 | } else {
581 | console.log(`[IaC Comments] Remote comment ${oComment.id} is newer than local version, will not push it`);
582 | }
583 | return remoteComment.timestampModified < oComment.lastModified;
584 | });
585 | }).reduce((a, b) => a.concat(b), []);
586 |
587 | // Push local changes to remote database
588 | for (const thread of localThreads) {
589 | let tid = this.threadIdMap.get(thread);
590 | if (tid === undefined) {
591 | // This should never happen
592 | assert(false, "[IaC Comments] tid is undefined");
593 | return;
594 | }
595 | console.log("[IaC Comments] Pushing thread " + tid + " to remote database");
596 | let anchor = this.thread2anchor(thread);
597 | let timestamp = await this.rdb.getRemoteTimestamp();
598 | this.rdb.pushThread(tid, timestamp, false, util.absPathToRel(thread.uri.fsPath), anchor);
599 | }
600 |
601 | for (const comment of localComments) {
602 | let oComment = comment as NoteComment;
603 | if (oComment.parent === undefined) {
604 | // This should never happen
605 | assert(false, "[IaC Comments] oComment.parent is undefined");
606 | return;
607 | }
608 | let tid = this.threadIdMap.get(oComment.parent);
609 | if (tid === undefined) {
610 | // This should never happen
611 | assert(false, "[IaC Comments] tid is undefined");
612 | return;
613 | }
614 | console.log("[IaC Comments] Pushing comment " + oComment.id + " to remote database");
615 | let timestamp = await this.rdb.getRemoteTimestamp();
616 | this.rdb.pushComment(oComment.id, tid, oComment.body.toString(), oComment.author.name, timestamp, false);
617 | }
618 | }
619 |
620 | async dispose() {
621 | for (const disposable of this.disposables) {
622 | this.context.subscriptions.splice(this.context.subscriptions.indexOf(disposable), 1);
623 | await disposable.dispose();
624 | }
625 | this.disposed = true;
626 | }
627 | }
628 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const EXT_COMMON_NAME = "PoiEx";
2 | export const EXT_NAME = "poiex";
3 | export const SEMGREP_TIMEOUT_MS = 240 * 1000; // Semgrep timeout to be used when scanning HCL files
4 | export const SEMGREP_MAX_BUFFER = 1024 * 1024 * 20; // 20MB
5 | export const REMOTEDB_INIT_TIMEOUT_MS = 10 * 1000;
6 | export const RW_CHECK_COLLECTION = "readWriteCheckCollection";
7 | export const PROJECT_DIR_COLLECTION = "projectDir";
8 | export const PROJECT_DIR_DB = "projectDirDB.db";
9 | export const IAC_FOLDER_NAME = "poiex-data";
10 | export const DIAGNOSTICS_COLLECTION_PREFIX = "diagnostics_";
11 | export const COMMENTS_COLLECTION_PREFIX = "comments_";
12 | export const DIAGNOSTICS_CODENAME = 'poiex';
13 | export const IAC_POI_MESSAGE = "IaC Point Of Intersection:";
14 | export const INFRAMAP_TIMEOUT_MS = 10 * 1000;
15 | export const INFRAMAP_DOWNLOADED_STATENAME = "inframapDownloading";
16 | export const PROJECT_TREE_VIEW_TITLE = "PoiEx: Project List";
17 | export const PROJECT_TREE_VIEW_NO_PROJECTS_MESSAGE = "No projects found.";
18 | export const PROJECT_TREE_VIEW_DB_ERROR_MESSAGE = "Invalid DB configuration.";
19 |
20 | export const FLAG_UNFLAGGED = 0;
21 | export const FLAG_FALSE = 1;
22 | export const FLAG_HOT = 2;
23 | export const FLAG_RESOLVED = 3;
24 |
25 | export const INFRAMAP_RELEASES: { [id: string] : any; } = {
26 | "linux": {
27 | "url": "https://github.com/cycloidio/inframap/releases/download/v0.6.7/inframap-linux-amd64.tar.gz",
28 | "integrity": "sha384-a8ANZlhXEgA64SjPBtbJiV3T33/wmxFImZMJTJpWJmffNGDJwnu6/knZDsHtZNz3"
29 | },
30 | "darwin": {
31 | "url": "https://github.com/cycloidio/inframap/releases/download/v0.6.7/inframap-darwin-amd64.tar.gz",
32 | "integrity": "sha384-eSEnikqoFvap1D9sSl6Y7Ka9TEPnWBvvWJ9unHPMqJVcXeHgHMcmeTjZvJ4hYEZT"
33 | }
34 | };
35 |
36 | // Set this to true to use <= MongoDB 4.X
37 | export const IS_MONGO_4 = false;
--------------------------------------------------------------------------------
/src/db.ts:
--------------------------------------------------------------------------------
1 | import * as sqlite3 from 'sqlite3';
2 | import * as sqlite from 'sqlite';
3 | import { assert } from 'console';
4 |
5 | import * as util from './util';
6 |
7 | // Create a class called LocalDB
8 | export class LocalDB {
9 | // Create a private variable called db
10 | private db: sqlite.Database | null;
11 | path: string;
12 |
13 | // Create a constructor that takes a path to the database
14 | constructor(path: string) {
15 | // Create a new sqlite3 database
16 | this.db = null;
17 | this.path = path;
18 | }
19 |
20 | async init() {
21 | this.db = await sqlite.open({
22 | filename: this.path,
23 | driver: sqlite3.Database
24 | });
25 | await this.dbCreateTables();
26 | }
27 |
28 | // Create a function called close that closes the database
29 | async close() {
30 | if (this.db === null) {
31 | return;
32 | }
33 | console.log('[IaC LocalDB] Closing');
34 | let olddb = this.db;
35 | this.db = null;
36 | // Close, but not await. May at times create a memory leak
37 | // TODO: Fix memory leak
38 | olddb.close().then(() => {
39 | console.log('[IaC LocalDB] Closed');
40 | }).catch((err: any) => {
41 | console.log('[IaC LocalDB] FIXME Error closing db: ' + err);
42 | });
43 | }
44 |
45 | // Given a thread id, return a list of all comment ids that are children of that thread
46 | async getCommentsForThread(threadId: string): Promise {
47 | assert(this.db !== null, '[IaC LocalDB] db is null');
48 | if (this.db === null) { return []; }
49 |
50 | let comments: string[] = [];
51 | let stmt = await this.db.prepare('SELECT id FROM comments WHERE thread_id = ?');
52 | let rows = await stmt.all(threadId);
53 | await stmt.finalize();
54 | rows.forEach((row: any) => {
55 | comments.push(row.id);
56 | });
57 | return comments;
58 | }
59 |
60 | async dropAllTables() {
61 | assert(this.db !== null, '[IaC LocalDB] db is null');
62 | if (this.db === null) { return []; }
63 |
64 | let stmt1 = await this.db.prepare('DROP TABLE IF EXISTS comments');
65 | await stmt1.run();
66 | await stmt1.finalize();
67 | let stmt2 = await this.db.prepare('DROP TABLE IF EXISTS threads');
68 | await stmt2.run();
69 | await stmt2.finalize();
70 | let stmt3 = await this.db.prepare('DROP TABLE IF EXISTS diagnostics');
71 | await stmt3.run();
72 | await stmt3.finalize();
73 | }
74 |
75 | // DB
76 | async dbCreateTables() {
77 | assert(this.db !== null, '[IaC LocalDB] db is null');
78 | if (this.db === null) { return []; }
79 |
80 | let stmt;
81 | stmt = await this.db.prepare('CREATE TABLE IF NOT EXISTS comments (id TEXT PRIMARY KEY, thread_id TEXT, comment TEXT, timestamp_updated DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp_created DATETIME DEFAULT CURRENT_TIMESTAMP, user_created TEXT, deleted INTEGER DEFAULT 0)');
82 | await stmt.run();
83 | await stmt.finalize();
84 | stmt = await this.db.prepare('CREATE TABLE IF NOT EXISTS threads (id TEXT PRIMARY KEY, anchor TEXT, file_path TEXT, deleted INTEGER DEFAULT 0)');
85 | await stmt.run();
86 | await stmt.finalize();
87 | stmt = await this.db.prepare('CREATE TABLE IF NOT EXISTS diagnostics (id TEXT PRIMARY KEY, diagnostic TEXT, flag INTEGER, flag_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, anchor TEXT, file_path TEXT, timestamp_created DATETIME DEFAULT CURRENT_TIMESTAMP)');
88 | await stmt.run();
89 | await stmt.finalize();
90 | }
91 |
92 | // DB
93 | async dbGetThreads(): Promise<[any[], Map]> {
94 | assert(this.db !== null, '[IaC LocalDB] db is null');
95 | if (this.db === null) { return [[], new Map()]; }
96 |
97 | let threads: any[] = [];
98 | let comments: Map = new Map();
99 | let stmt = await this.db.prepare('SELECT * FROM threads WHERE deleted = 0');
100 | let rows = await stmt.all();
101 | await stmt.finalize();
102 | rows.forEach((row: any) => {
103 | threads.push(row);
104 | comments.set(row.id, []);
105 | });
106 | stmt = await this.db.prepare('SELECT * FROM comments WHERE deleted = 0');
107 | rows = await stmt.all();
108 | rows.forEach((row: any) => {
109 | comments.get(row.thread_id).push(row);
110 | });
111 | return [threads, comments];
112 | }
113 |
114 | // DB
115 | async deleteOrphanComments(): Promise {
116 | assert(this.db !== null, '[IaC LocalDB] db is null');
117 | if (this.db === null) { return; }
118 |
119 | // Delete comments that are not associated with a thread
120 | // let stmt = await this.db.prepare('DELETE FROM comments WHERE thread_id NOT IN (SELECT id FROM threads)');
121 | let stmt = await this.db.prepare('UPDATE comments SET comment = NULL, timestamp_updated = NULL, timestamp_created = NULL, user_created = NULL, deleted = 1 WHERE thread_id NOT IN (SELECT id FROM threads)');
122 | await stmt.run();
123 | await stmt.finalize();
124 | }
125 |
126 | // DB
127 | async createOrReplaceThread(tid: string, anchor: string, filePath: string): Promise {
128 | assert(this.db !== null, '[IaC LocalDB] db is null');
129 | if (this.db === null) { return; }
130 |
131 | let stmt = await this.db.prepare('INSERT OR REPLACE INTO threads (id, anchor, file_path) VALUES (?, ?, ?)');
132 | await stmt.run(tid, anchor, filePath);
133 | await stmt.finalize();
134 | }
135 |
136 | // DB
137 | async dbCreateOrReplaceComment(cid: string, tid: string, comment: string, userCreated: string, lastModified: number | null = null): Promise {
138 | assert(this.db !== null, '[IaC LocalDB] db is null');
139 | if (this.db === null) { return; }
140 |
141 | lastModified = lastModified === null ? (Date.now() / 1000) : lastModified;
142 | let stmt = await this.db.prepare('INSERT OR REPLACE INTO comments (id, thread_id, comment, user_created, timestamp_updated) VALUES (?, ?, ?, ?, ?)');
143 | await stmt.run(cid, tid, comment, userCreated, lastModified);
144 | await stmt.finalize();
145 | }
146 |
147 | // DB
148 | async dbDeleteComments(cids: string[]): Promise {
149 | assert(this.db !== null, '[IaC LocalDB] db is null');
150 | if (this.db === null) { return; }
151 |
152 | // Delete comments from database
153 | // let stmt = await this.db.prepare('DELETE FROM comments WHERE id IN (?)');
154 | let stmt = await this.db.prepare('UPDATE comments SET comment = NULL, timestamp_updated = NULL, timestamp_created = NULL, user_created = NULL, deleted = 1 WHERE id IN (?)');
155 | await stmt.run(cids);
156 | await stmt.finalize();
157 | }
158 |
159 | // DB
160 | async dbDeleteTid(tid: string): Promise {
161 | assert(this.db !== null, '[IaC LocalDB] db is null');
162 | if (this.db === null) { return; }
163 |
164 | // Delete thread from database
165 | // let stmt = this.db.prepare('DELETE FROM threads WHERE id = ?');
166 | let stmt = await this.db.prepare('UPDATE threads SET anchor = NULL, file_path = NULL, deleted = 1 WHERE id = ?');
167 | await stmt.run(tid);
168 | await stmt.finalize();
169 |
170 | // stmt = this.db.prepare('DELETE FROM comments WHERE thread_id = ?');
171 | stmt = await this.db.prepare('UPDATE comments SET comment = NULL, timestamp_updated = NULL, timestamp_created = NULL, user_created = NULL, deleted = 1 WHERE thread_id = ?');
172 | await stmt.run(tid);
173 | await stmt.finalize();
174 |
175 | // Delete orphan comments
176 | await this.deleteOrphanComments();
177 | }
178 |
179 | async dbGetDeletedThreads(): Promise {
180 | assert(this.db !== null, '[IaC LocalDB] db is null');
181 | if (this.db === null) { return []; }
182 |
183 | let stmt = await this.db.prepare('SELECT id FROM threads WHERE deleted = 1');
184 | let res = await stmt.all();
185 | await stmt.finalize();
186 | return res.map((row: any) => row.id);
187 | }
188 |
189 | async dbGetDeletedComments(): Promise<[string, string][]> {
190 | assert(this.db !== null, '[IaC LocalDB] db is null');
191 | if (this.db === null) { return []; }
192 |
193 | let stmt = await this.db.prepare('SELECT id, thread_id FROM comments WHERE deleted = 1');
194 | let rows = await stmt.all();
195 | await stmt.finalize();
196 | return rows.map((row: any) => [row.id, row.thread_id]);
197 | }
198 |
199 | async dbClearDiagnostics(): Promise {
200 | assert(this.db !== null, '[IaC LocalDB] db is null');
201 | if (this.db === null) { return; }
202 |
203 | let stmt = await this.db.prepare('DELETE FROM diagnostics');
204 | await stmt.run();
205 | await stmt.finalize();
206 | }
207 |
208 | async dbDeleteDiagnostic(uuid: string): Promise {
209 | assert(this.db !== null, '[IaC LocalDB] db is null');
210 | if (this.db === null) { return; }
211 |
212 | let stmt = await this.db.prepare('DELETE FROM diagnostics WHERE id = ?');
213 | await stmt.run(uuid);
214 | await stmt.finalize();
215 | }
216 |
217 | async dbGetDiagnostics(): Promise {
218 | assert(this.db !== null, '[IaC LocalDB] db is null');
219 | if (this.db === null) { return []; }
220 |
221 | let stmt = await this.db.prepare('SELECT id, diagnostic, flag, flag_timestamp, anchor, file_path, timestamp_created FROM diagnostics');
222 | let res = await stmt.all();
223 | await stmt.finalize();
224 | return res;
225 | }
226 |
227 | async dbCreateOrReplaceDiagnosticWithId(uuid: string, diagnostic: string, anchor: string, filePath: string, flag: number, lastModified: number | null = null): Promise {
228 | assert(uuid !== null && uuid !== undefined, '[IaC LocalDB] uuid must be defined');
229 | assert(diagnostic !== null && diagnostic !== undefined, '[IaC LocalDB] diagnostic must be defined');
230 | assert(anchor !== null && anchor !== undefined, '[IaC LocalDB] anchor must be defined');
231 | assert(filePath !== null && filePath !== undefined, '[IaC LocalDB] filePath must be defined');
232 | assert(this.db !== null, '[IaC LocalDB] db is null');
233 | if (this.db === null) { return; }
234 |
235 | lastModified = lastModified === null ? (Date.now() / 1000) : lastModified;
236 | let stmt = await this.db.prepare('INSERT OR REPLACE INTO diagnostics (id, diagnostic, flag, flag_timestamp, anchor, file_path, timestamp_created) VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?, ?)');
237 | console.log(`[IaC LocalDB] Binding sqlite parameters for diagnostic: ${uuid}, ${diagnostic}, ${anchor}, ${filePath}, ${lastModified}`);
238 | await stmt.run(uuid, diagnostic, flag, anchor, filePath, lastModified);
239 | await stmt.finalize();
240 | }
241 |
242 | async dbUpdateDiagnosticFlag(uuid: string, flag: number): Promise {
243 | assert(uuid !== null && uuid !== undefined, '[IaC LocalDB] uuid must be defined');
244 | assert(flag !== null && flag !== undefined, '[IaC LocalDB] flag must be defined');
245 | assert(this.db !== null, '[IaC LocalDB] db is null');
246 | if (this.db === null) { return; }
247 |
248 | let stmt = await this.db.prepare('UPDATE diagnostics SET flag = ?, flag_timestamp = CURRENT_TIMESTAMP WHERE id = ?');
249 | await stmt.run(flag, uuid);
250 | await stmt.finalize();
251 | }
252 |
253 | async dbCreateOrReplaceDiagnostic(diagnostic: string, anchor: string, filePath: string, flag: number): Promise {
254 | let uuid = util.genUUID();
255 | await this.dbCreateOrReplaceDiagnosticWithId(uuid, diagnostic, anchor, filePath, flag);
256 | return uuid;
257 | }
258 | }
--------------------------------------------------------------------------------
/src/diagnostics.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import { assert } from 'console';
4 | import { LocalDB } from './db';
5 | import { ANCHOR_LINES, findClosestAnchor } from './anchor';
6 | import { RemoteDB } from './remote';
7 | import * as util from './util';
8 | import { DIAGNOSTICS_CODENAME } from './constants';
9 | import * as constants from './constants';
10 | import { cursorTo } from 'readline';
11 |
12 | export class SastInfo implements vscode.CodeActionProvider {
13 | // Create a map of diagnostic to code action
14 | private _diagnosticCodeActions = new Map();
15 |
16 | public static readonly providedCodeActionKinds = [
17 | vscode.CodeActionKind.QuickFix
18 | ];
19 |
20 | provideCodeActions(document: vscode.TextDocument, range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, token: vscode.CancellationToken): vscode.CodeAction[] {
21 | let codeActions: vscode.CodeAction[] = [];
22 | this._diagnosticCodeActions.forEach((value, key) => {
23 | if (context.diagnostics.includes(key)) {
24 | codeActions = codeActions.concat(value);
25 | }
26 | });
27 | console.log(codeActions.map((codeAction) => codeAction.title));
28 | return codeActions;
29 | /*return context.diagnostics
30 | .filter(diagnostic => diagnostic.code === DIAGNOSTICS_CODENAME)
31 | .filter(diagnostic => this._diagnosticCodeActions.has(diagnostic))
32 | .map(diagnostic => this._diagnosticCodeActions.get(diagnostic) as vscode.CodeAction);*/
33 | }
34 | }
35 |
36 | export class IaCDiagnostics {
37 | private _diagnostics: vscode.DiagnosticCollection;
38 | private _diagnosticsIds: Map = new Map();
39 | private _rawDiagnostics: Map = new Map();
40 | private _sastInfo: any;
41 | private _db: LocalDB;
42 | private _rdb: RemoteDB | undefined;
43 | private _currentlyPulling: boolean = false;
44 | private _disposables: vscode.Disposable[] = [];
45 | private _context: vscode.ExtensionContext;
46 | private _disposed: boolean = false;
47 | private _onDiagnosticUpdateList: Array<(diagnostics: Map) => void> = [];
48 |
49 | constructor(context: vscode.ExtensionContext, db: LocalDB, rdb: RemoteDB | undefined) {
50 | this._db = db;
51 | this._rdb = rdb;
52 | this._context = context;
53 | this._diagnostics = vscode.languages.createDiagnosticCollection(DIAGNOSTICS_CODENAME);
54 | context.subscriptions.push(this._diagnostics);
55 | this._disposables.push(this._diagnostics);
56 |
57 |
58 | this._sastInfo = new SastInfo();
59 | let codeActionProvider = vscode.languages.registerCodeActionsProvider('*', this._sastInfo, {
60 | providedCodeActionKinds: SastInfo.providedCodeActionKinds
61 | });
62 | context.subscriptions.push(codeActionProvider);
63 |
64 | // Command to delete an arbitrary finding
65 | let disposableCommand = vscode.commands.registerCommand(`${constants.EXT_NAME}.deleteFinding`, (uuid: string) => {
66 | this.deleteFinding(uuid);
67 | });
68 | context.subscriptions.push(disposableCommand);
69 | this._disposables.push(disposableCommand);
70 |
71 | // Command to flag an arbitrary finding
72 | disposableCommand = vscode.commands.registerCommand(`${constants.EXT_NAME}.flagFinding`, (uuid: string, flag: number) => {
73 | this.flagFinding(uuid, flag);
74 | });
75 | context.subscriptions.push(disposableCommand);
76 | this._disposables.push(disposableCommand);
77 |
78 | if (this._rdb === undefined) {
79 | return;
80 | }
81 | this._rdb.onDbReady(async () => {
82 | if (this._disposed) { return; }
83 | await this.syncRdb();
84 | });
85 | this._rdb.onDiagnosticsUpdate(async (diagnosticsToStore: any[]) => {
86 | if (this._disposed) { return; }
87 | await this.safeRdbPull(diagnosticsToStore);
88 | });
89 | }
90 |
91 | public onDiagnosticsUpdate(callback: (diagnostics: Map) => void): void {
92 | this._onDiagnosticUpdateList.push(callback);
93 | callback(this._rawDiagnostics);
94 | }
95 |
96 | private diagnosticsUpdate(): void {
97 | this._onDiagnosticUpdateList.forEach((callback) => {
98 | callback(this._rawDiagnostics);
99 | });
100 | }
101 |
102 | dispose() {
103 | this._diagnostics.clear();
104 | this._disposables.forEach((disposable) => {
105 | this._context.subscriptions.splice(this._context.subscriptions.indexOf(disposable), 1);
106 | disposable.dispose();
107 | });
108 | this._disposed = true;
109 | }
110 |
111 | // Will check if a sync is currently in progress, and if not, will start one
112 | private async safeRdbPull(arg: any) {
113 | if (this._currentlyPulling) {
114 | console.log("[IaC Diagnostics] Sync already in progress, skipping");
115 | return;
116 | }
117 | this._currentlyPulling = true;
118 | await this.rdbPull(arg);
119 | this._currentlyPulling = false;
120 | }
121 |
122 | private async rdbPull(diagnosticsToStore: any[], isReplaceAll: boolean = true): Promise {
123 | console.log("rdbPull");
124 | // Clear and rewrite local sqlite database
125 | if (isReplaceAll) {
126 | await this._db.dbClearDiagnostics();
127 | }
128 | for (let i = 0; i < diagnosticsToStore.length; i++) {
129 | await this._db.dbCreateOrReplaceDiagnosticWithId(diagnosticsToStore[i].id, diagnosticsToStore[i].diagnostic, diagnosticsToStore[i].anchor, diagnosticsToStore[i].file_path, diagnosticsToStore[i].flag);
130 | }
131 | // Load diagnostics from DB
132 | this.loadDiagnosticsFromDB();
133 | }
134 |
135 | private async deleteFinding(uuid: string): Promise {
136 | console.log(`deleteFinding(${uuid})`);
137 | await this._db.dbDeleteDiagnostic(uuid);
138 | if ((this._rdb === undefined) || (!this._rdb.isRemoteReady())) {
139 | await this.loadDiagnosticsFromDB();
140 | return;
141 | }
142 | await this._rdb.deleteDiagnostics(null, [uuid]);
143 | await this.loadDiagnosticsFromDB();
144 | return;
145 | }
146 |
147 | private async flagFinding(uuid: string, flag: number): Promise {
148 | console.log(`flagFinding(${uuid}, ${flag})`);
149 | await this._db.dbUpdateDiagnosticFlag(uuid, flag);
150 | await this.syncRdb();
151 | await this.loadDiagnosticsFromDB();
152 | return;
153 | }
154 |
155 | private async syncRdb() {
156 | if (this._rdb === undefined) {
157 | return;
158 | }
159 | if (!this._rdb.isRemoteReady()) {
160 | return;
161 | }
162 | let diagnosticsToPush = await this._db.dbGetDiagnostics();
163 | console.log(`syncRdb(${diagnosticsToPush.length})`);
164 | let [diagnostics, isReplaceAll] = await this._rdb.syncDiagnostics(diagnosticsToPush);
165 | if (diagnostics === undefined) {
166 | return;
167 | }
168 | await this.rdbPull(diagnostics, isReplaceAll);
169 | }
170 |
171 | // TODO: This is quadratical, make it linear
172 | private async refreshDiagnostics(doc: vscode.TextDocument, semgrepParsed: any): Promise {
173 | console.log("[Semgrep] OK4 ");
174 |
175 | const diagnostics: vscode.Diagnostic[] = [];
176 |
177 | // TODO: check these fields exists in the parsed json. Show error if not.
178 | for (let e of semgrepParsed["parsed"]["results"]) {
179 | if (vscode.workspace.workspaceFolders === undefined) {
180 | assert(false, "Workspace folder is undefined, but was checked before.");
181 | return;
182 | }
183 | const absPath = util.relPathToAbs(e["path"]);
184 | if (absPath !== doc.uri.fsPath) {
185 | continue;
186 | }
187 | let diagUuid = util.genUUID();
188 |
189 | const diagnostic = this.createDiagnostic(
190 | diagUuid,
191 | e["start"]["line"] - 1,
192 | e["start"]["col"] - 1,
193 | e["end"]["line"] - 1,
194 | e["end"]["col"] - 1,
195 | e["extra"]["message"],
196 | e["extra"]["metadata"]["source"],
197 | e["extra"]["metadata"]["severity"],
198 | e["extra"]["metadata"]["references"],
199 | semgrepParsed["source"],
200 | constants.FLAG_UNFLAGGED
201 | );
202 | diagnostics.push(diagnostic);
203 | console.log(e["path"]);
204 |
205 | let anchorLine = e["start"]["line"] - 1;
206 | let anchorText = "";
207 | let numLinesFromAnchor = 0;
208 | let anchorTextLineBegin = Math.max(anchorLine - ANCHOR_LINES, 0);
209 | let anchorTextLineEnd = Math.min(anchorLine + ANCHOR_LINES, doc.lineCount);
210 | let anchorText1 = doc.getText(new vscode.Range(anchorTextLineBegin, 0, anchorLine, 0));
211 | let anchorText2 = doc.getText(new vscode.Range(anchorLine, 0, anchorTextLineEnd, 0));
212 |
213 | // Limit size of anchor text to 1000 characters
214 | anchorText1 = anchorText1.substring(Math.max(anchorText1.length - 500, 0));
215 | anchorText2 = anchorText2.substring(0, 500);
216 | numLinesFromAnchor = anchorText1.split(/\r\n|\r|\n/).length - 1;
217 | anchorText = anchorText1 + anchorText2;
218 | let anchor = JSON.stringify({ "line": anchorLine, "text": anchorText, "num": numLinesFromAnchor });
219 |
220 | // Serialize diagnostic
221 | let serializedDiagnostic = JSON.stringify({
222 | "message": e["extra"]["message"],
223 | "severity": e["extra"]["metadata"]["severity"],
224 | "source": e["extra"]["metadata"]["source"],
225 | "relatedInformation": e["extra"]["metadata"]["references"]
226 | });
227 |
228 | await this._db.dbCreateOrReplaceDiagnosticWithId(diagUuid, serializedDiagnostic, anchor, e["path"], constants.FLAG_UNFLAGGED);
229 | this._diagnosticsIds.set(diagUuid, diagnostic);
230 |
231 | let rawDiagnostic = {
232 | "id": diagUuid,
233 | "path": e["path"],
234 | "line": anchorLine,
235 | "message": e["extra"]["message"],
236 | "source": e["extra"]["metadata"]["source"],
237 | "severity": e["extra"]["metadata"]["severity"],
238 | "references": e["extra"]["metadata"]["references"],
239 | "from": semgrepParsed["source"]
240 | };
241 | this._rawDiagnostics.set(diagUuid, rawDiagnostic);
242 | }
243 |
244 | this._diagnostics.set(doc.uri, diagnostics);
245 | this.diagnosticsUpdate();
246 | console.log("[Semgrep] OK5 ");
247 | }
248 |
249 | async clearLocalDiagnostics(): Promise {
250 | this._diagnostics.clear();
251 | this._sastInfo._diagnosticCodeActions.clear();
252 | this._diagnosticsIds.clear();
253 | this._rawDiagnostics.clear();
254 | await this._db.dbClearDiagnostics();
255 | this.diagnosticsUpdate();
256 | }
257 |
258 | async clearDiagnostics(): Promise {
259 | await this.clearLocalDiagnostics();
260 | await this.syncRdb();
261 | }
262 |
263 | async loadDiagnostics(): Promise {
264 | await this.loadDiagnosticsFromDB();
265 | await this.syncRdb();
266 | }
267 |
268 | private msgToFlagId(msg: string): number {
269 | if (msg.startsWith("🆕")) {
270 | return constants.FLAG_UNFLAGGED;
271 | }
272 | else if (msg.startsWith("✅")) {
273 | return constants.FLAG_RESOLVED;
274 | }
275 | else if (msg.startsWith("❌")) {
276 | return constants.FLAG_FALSE;
277 | }
278 | else if (msg.startsWith("🔥")) {
279 | return constants.FLAG_HOT;
280 | }
281 | else {
282 | return constants.FLAG_UNFLAGGED;
283 | }
284 | }
285 |
286 | async loadDiagnosticsFromDB(): Promise {
287 | let dbDiagnostics = await this._db.dbGetDiagnostics();
288 | let diagnosticsToClear = [...this._diagnosticsIds.keys()];
289 |
290 | console.log("[Diagnostics] Db has ");
291 | console.log(dbDiagnostics);
292 | for (let i = 0; i < dbDiagnostics.length; i++) {
293 | let dbDiagnostic = dbDiagnostics[i].diagnostic;
294 | let dbAnchor = dbDiagnostics[i].anchor;
295 | let dbPath = dbDiagnostics[i].file_path;
296 | let dbId = dbDiagnostics[i].id;
297 | let dbFlag = dbDiagnostics[i].flag;
298 |
299 | console.log("Diagnostic id: " + dbId);
300 | console.log("All diagnostic ids: " + [...this._diagnosticsIds.keys()]);
301 | if (this._diagnosticsIds.has(dbId)) {
302 | let diagMsg = this._diagnosticsIds.get(dbId)?.message;
303 | diagnosticsToClear = diagnosticsToClear.filter((e) => e !== dbId);
304 | if ((diagMsg == undefined) || (dbFlag == this.msgToFlagId(diagMsg))) {
305 | console.log("Skipping updating an existing diagnostic");
306 | continue;
307 | }
308 | else {
309 | console.log("Updating an existing diagnostic");
310 | await this.clearOneDiagnostic(this._diagnosticsIds.get(dbId) as vscode.Diagnostic, dbId);
311 | }
312 | }
313 |
314 | // Deserialize diagnostic
315 | let deserializedDiagnostic = JSON.parse(dbDiagnostic);
316 | let deserializedAnchor = JSON.parse(dbAnchor);
317 |
318 | const doc = await this.getDocumentFromSemgrepPath(dbPath);
319 | if (doc === undefined) {
320 | vscode.window.showWarningMessage('Semgrep result references a file that is not present in workspace.');
321 | return;
322 | }
323 | let anchorLine = findClosestAnchor(deserializedAnchor.text, doc.getText());
324 | if (anchorLine === -1) {
325 | anchorLine = deserializedAnchor.line;
326 | }
327 | else {
328 | anchorLine = anchorLine + deserializedAnchor.num;
329 | }
330 | console.log("Closest anchor: " + anchorLine);
331 |
332 | // Create diagnostic
333 | const diagnostic = this.createDiagnostic(
334 | dbId,
335 | deserializedAnchor.line,
336 | 0,
337 | deserializedAnchor.line + 1,
338 | 12345,
339 | deserializedDiagnostic.message,
340 | deserializedDiagnostic.source,
341 | deserializedDiagnostic.severity,
342 | deserializedDiagnostic.relatedInformation,
343 | "semgrep-remote",
344 | dbFlag
345 | );
346 | // Add diagnostic to collection
347 | let curDiag = this._diagnostics.get(doc.uri);
348 | if (curDiag !== undefined) {
349 | curDiag = curDiag.concat(diagnostic);
350 | }
351 | else {
352 | curDiag = [diagnostic];
353 | }
354 | this._diagnostics.set(doc.uri, curDiag);
355 |
356 | // Add diagnostic to SastInfo
357 | let action = this.makeCodeActions(dbId, deserializedDiagnostic.source, diagnostic, dbFlag);
358 | this._sastInfo._diagnosticCodeActions.set(diagnostic, action);
359 | // Add diagnostic to list of diagnostics
360 |
361 | this._diagnosticsIds.set(dbId, diagnostic);
362 |
363 | let rawDiagnostic = {
364 | "id": dbId,
365 | "path": dbPath,
366 | "line": deserializedAnchor.line,
367 | "message": deserializedDiagnostic.message,
368 | "source": deserializedDiagnostic.source,
369 | "severity": deserializedDiagnostic.severity,
370 | "references": deserializedDiagnostic.relatedInformation,
371 | "from": "semgrep-remote"
372 | };
373 | this._rawDiagnostics.set(dbId, rawDiagnostic);
374 | }
375 |
376 | // Clear diagnostics that are not in the db
377 | console.log("Diagnostics to clear: " + diagnosticsToClear);
378 | for (let i = 0; i < diagnosticsToClear.length; i++) {
379 | let diag = this._diagnosticsIds.get(diagnosticsToClear[i]);
380 | await this.clearOneDiagnostic(diag as vscode.Diagnostic, diagnosticsToClear[i]);
381 | }
382 |
383 | this.diagnosticsUpdate();
384 | }
385 |
386 | async clearOneDiagnostic(diag: vscode.Diagnostic, uuid: string): Promise {
387 | console.log("Clearing diagnostic: " + diag);
388 | if (diag === undefined) {
389 | console.log("Diagnostic is undefined");
390 | return;
391 | }
392 | let diagnosticUri = null;
393 | this._diagnostics.forEach((key, value) => {
394 | if (value.includes(diag as vscode.Diagnostic)) {
395 | diagnosticUri = key;
396 | }
397 | });
398 | if (diagnosticUri !== null) {
399 | let diagnostics = this._diagnostics.get(diagnosticUri);
400 | if (diagnostics !== undefined) {
401 | console.log(`Removing diagnostic from collection ${diagnostics.length} > ${diagnostics.filter((e) => e !== diag).length}`);
402 | this._diagnostics.set(diagnosticUri, diagnostics.filter((e) => e !== diag));
403 | }
404 | }
405 |
406 | this._diagnosticsIds.delete(uuid);
407 | this._rawDiagnostics.delete(uuid);
408 | this._sastInfo._diagnosticCodeActions.delete(diag);
409 | }
410 |
411 | // TODO: fetch from all workspaces, not just the first one
412 | async getDocumentFromSemgrepPath(semgrepPath: string): Promise {
413 | // Get document from path
414 | if (vscode.workspace.workspaceFolders === undefined) {
415 | return undefined;
416 | }
417 | console.log(vscode.workspace.workspaceFolders);
418 | const absPath = util.relPathToAbs(semgrepPath);
419 |
420 | // Match file only within workspace
421 | let relPattern = new vscode.RelativePattern(vscode.workspace.workspaceFolders[0], semgrepPath);
422 | let uris = await vscode.workspace.findFiles(relPattern, null, 1);
423 | console.log(uris);
424 |
425 | if (uris.length === 0) {
426 | console.log(absPath);
427 | return undefined;
428 | }
429 | const doc = await vscode.workspace.openTextDocument(uris[0]);
430 | return doc;
431 | }
432 |
433 | async loadDiagnosticsFromSemgrep(semgrepParsed: any): Promise {
434 | // Create set of paths
435 | const docs = new Set();
436 | await Promise.all(semgrepParsed["parsed"]["results"].map(async (e: any) => {
437 | let doc = await this.getDocumentFromSemgrepPath(e["path"]);
438 | if (doc === undefined) {
439 | vscode.window.showWarningMessage('Semgrep result references a file that is not present in workspace.');
440 | return;
441 | }
442 | docs.add(doc);
443 | }));
444 |
445 | for (let doc of docs) {
446 | await this.refreshDiagnostics(doc, semgrepParsed);
447 | }
448 | await this.syncRdb();
449 | }
450 |
451 | private createDiagnostic(uuid: string, lineStart: number, colStart: number, lineEnd: number, colEnd: number, message: string, externalUrl: string, severity: string, references: string[], source: string, flag: number): vscode.Diagnostic {
452 | // create range that represents, where in the document the word is
453 | const range = new vscode.Range(lineStart, colStart, lineEnd, colEnd);
454 |
455 | // Convert severity to vscode.DiagnosticSeverity
456 | let severityVsc: vscode.DiagnosticSeverity;
457 | switch (severity) {
458 | case "ERROR":
459 | severityVsc = vscode.DiagnosticSeverity.Error;
460 | break;
461 | case "WARNING":
462 | severityVsc = vscode.DiagnosticSeverity.Warning;
463 | break;
464 | case "INFO":
465 | severityVsc = vscode.DiagnosticSeverity.Information;
466 | break;
467 | default:
468 | severityVsc = vscode.DiagnosticSeverity.Information;
469 | break;
470 | }
471 |
472 | // Add flag to message
473 | switch (flag) {
474 | case constants.FLAG_UNFLAGGED:
475 | message = "🆕 " + message;
476 | break;
477 | case constants.FLAG_RESOLVED:
478 | message = "✅ " + message;
479 | break;
480 | case constants.FLAG_FALSE:
481 | message = "❌ " + message;
482 | break;
483 | case constants.FLAG_HOT:
484 | message = "🔥 " + message;
485 | break;
486 | default:
487 | break;
488 | }
489 |
490 | const diagnostic = new vscode.Diagnostic(range, message, severityVsc);
491 | diagnostic.code = DIAGNOSTICS_CODENAME;
492 | switch (source) {
493 | case "semgrep-imported":
494 | diagnostic.source = "Semgrep [imported] ";
495 | break;
496 | case "semgrep-local":
497 | diagnostic.source = "Semgrep [local] ";
498 | break;
499 | case "semgrep-remote":
500 | diagnostic.source = "Semgrep [remote] ";
501 | break;
502 | default:
503 | diagnostic.source = "Unknown source ";
504 | break;
505 | }
506 |
507 | if (references === undefined) {
508 | references = [];
509 | }
510 | diagnostic.relatedInformation = references.map(ref => {
511 | return new vscode.DiagnosticRelatedInformation(
512 | new vscode.Location(vscode.Uri.parse(ref), new vscode.Position(0, 0)),
513 | 'External reference (open in browser)'
514 | );
515 | });
516 | diagnostic.tags = [vscode.DiagnosticTag.Unnecessary];
517 |
518 | // Add a code action
519 | let action = this.makeCodeActions(uuid, externalUrl, diagnostic, flag);
520 | this._sastInfo._diagnosticCodeActions.set(diagnostic, action);
521 |
522 | return diagnostic;
523 | }
524 |
525 | private makeCodeActions(diagUuid: string, externalUrl: string, diagnostic: vscode.Diagnostic, flag: number): vscode.CodeAction[] {
526 | const action = new vscode.CodeAction('Learn more on this finding', vscode.CodeActionKind.QuickFix);
527 | action.command = { arguments: [externalUrl], command: `${constants.EXT_NAME}.openLink`, title: 'Learn more about this finding', tooltip: 'This will open an external page.' };
528 | action.diagnostics = [diagnostic];
529 | action.isPreferred = true;
530 |
531 | const action2 = new vscode.CodeAction('Delete this finding', vscode.CodeActionKind.QuickFix);
532 | action2.command = { arguments: [diagUuid], command: `${constants.EXT_NAME}.deleteFinding`, title: 'Delete this finding', tooltip: 'This will delete this finding from the database.' };
533 | action2.diagnostics = [diagnostic];
534 | action2.isPreferred = false;
535 |
536 | if (flag == constants.FLAG_UNFLAGGED) {
537 | const action3 = new vscode.CodeAction('Mark this finding as ✅ resolved', vscode.CodeActionKind.QuickFix);
538 | action3.command = { arguments: [diagUuid, constants.FLAG_RESOLVED], command: `${constants.EXT_NAME}.flagFinding`, title: 'Mark this finding as resolved', tooltip: 'This will mark this finding as resolved.' };
539 | action3.diagnostics = [diagnostic];
540 | action3.isPreferred = false;
541 |
542 | const action4 = new vscode.CodeAction('Mark this finding as ❌ false positive', vscode.CodeActionKind.QuickFix);
543 | action4.command = { arguments: [diagUuid, constants.FLAG_FALSE], command: `${constants.EXT_NAME}.flagFinding`, title: 'Mark this finding as false positive', tooltip: 'This will mark this finding as false positive.' };
544 | action4.diagnostics = [diagnostic];
545 | action4.isPreferred = false;
546 |
547 | const action5 = new vscode.CodeAction('Mark this finding as 🔥 hot', vscode.CodeActionKind.QuickFix);
548 | action5.command = { arguments: [diagUuid, constants.FLAG_HOT], command: `${constants.EXT_NAME}.flagFinding`, title: 'Mark this finding as hot', tooltip: 'This will mark this finding as hot.' };
549 | action5.diagnostics = [diagnostic];
550 | action5.isPreferred = false;
551 |
552 | return [action, action2, action3, action4, action5];
553 | }
554 | else {
555 | const action3 = new vscode.CodeAction('Unmark ↩️ this finding', vscode.CodeActionKind.QuickFix);
556 | action3.command = { arguments: [diagUuid, constants.FLAG_UNFLAGGED], command: `${constants.EXT_NAME}.flagFinding`, title: 'Unmark this finding', tooltip: 'This will unmark this finding.' };
557 | action3.diagnostics = [diagnostic];
558 | action3.isPreferred = false;
559 |
560 | return [action, action2, action3];
561 | }
562 | }
563 | }
--------------------------------------------------------------------------------
/src/encryption.ts:
--------------------------------------------------------------------------------
1 | import * as jose from 'jose';
2 |
3 | export class IaCEncryption {
4 | private secret: Uint8Array | jose.KeyLike | null = null;
5 |
6 | constructor() {
7 |
8 | }
9 |
10 | async setKey(b64secret: string | null = null) {
11 | if (b64secret === null) {
12 | this.secret = await jose.generateSecret('HS256', {extractable: true});
13 | return;
14 | }
15 | this.secret = jose.base64url.decode(b64secret);
16 | }
17 |
18 | static async genKey(): Promise {
19 | let secret = await jose.generateSecret('HS256', {extractable: true});
20 | let secretArr = (secret as any).export();
21 | console.log(secretArr);
22 | return jose.base64url.encode(secretArr);
23 | }
24 |
25 | async checkKey(token: string): Promise {
26 | if (this.secret === null) {
27 | return null;
28 | }
29 | try {
30 | let payload: any = await this.decrypt(token);
31 | return payload.uuid;
32 | } catch (e) {
33 | return null;
34 | }
35 | }
36 |
37 | async encrypt(data: {}): Promise {
38 | if (this.secret === null) {
39 | throw new Error("Encryption key not set");
40 | }
41 | const jwt = await new jose.EncryptJWT(data).setProtectedHeader({ alg: 'dir', enc: 'A128CBC-HS256' }).encrypt(this.secret);
42 | return jwt;
43 | }
44 |
45 | async decrypt(jwt: string): Promise<{}> {
46 | if (this.secret === null) {
47 | throw new Error("Encryption key not set");
48 | }
49 | const { payload, protectedHeader } = await jose.jwtDecrypt(jwt, this.secret);
50 | return payload;
51 | }
52 |
53 | async optionallyEncryptString(data: string | null): Promise {
54 | if (this.secret === null) {
55 | return data;
56 | }
57 | if (data === null) { return null; }
58 | return await this.encrypt({"p": data});
59 | }
60 |
61 | async optionallyDecryptString(data: string | null): Promise {
62 | if (this.secret === null) {
63 | return data;
64 | }
65 | if (data === null) { return null; }
66 | let payload: any = await this.decrypt(data);
67 | return payload.p;
68 | }
69 |
70 | async exportKey(): Promise {
71 | if (this.secret === null) {
72 | throw new Error("Encryption key not set");
73 | }
74 | return jose.base64url.encode(this.secret as Uint8Array);
75 | }
76 |
77 | dispose() {
78 | this.secret = null;
79 | }
80 | }
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import { assert } from 'console';
3 | import * as path from 'path';
4 | import * as fs from 'fs';
5 |
6 | import { IaCProjectDir } from './projects';
7 | import { IaCWebviewManager } from './iacview';
8 | import * as constants from './constants';
9 | import { handleOpenSemgrepJson, handleStartSemgrepScan } from './util';
10 | import * as util from './util';
11 | import { IaCDiagnostics } from './diagnostics';
12 | import { LocalDB } from './db';
13 | import { RemoteDB } from './remote';
14 | import * as comments from './comments';
15 | import { IaCEncryption } from './encryption';
16 | import * as tree from './tree';
17 |
18 | let db: LocalDB;
19 | let rdb: RemoteDB;
20 | let mComments: comments.IaCComments;
21 | let pdb: IaCProjectDir;
22 | let projectDisposables: vscode.Disposable[] = [];
23 | let projectClosing: boolean = false;
24 | let projectTreeView: tree.ProjectTreeViewManager;
25 |
26 | export async function initLocalDb(dbDir: string, projectUuid: string) {
27 | // Create sqlite3 database in storage directory
28 | let dbFilename = path.basename(`/${constants.EXT_NAME}-${projectUuid}.db`);
29 | let dbPath = path.join(dbDir, dbFilename);
30 | db = new LocalDB(dbPath);
31 | await db.init();
32 | }
33 |
34 | function updateStatusBar(rdb: RemoteDB, remoteStatus: vscode.StatusBarItem) {
35 | remoteStatus.command = `${constants.EXT_NAME}.statusBarButton`;
36 | if (rdb.settingsEnabledAndConfigured()) {
37 | let dbName = rdb.getDbName();
38 | remoteStatus.text = `$(compass-dot) PoiEx sync: $(ellipsis) Connecting (${dbName})`;
39 | if (rdb.isRemoteReady()) {
40 | remoteStatus.text = `$(compass-active) PoiEx sync: $(arrow-swap) Connected (${dbName})`;
41 | }
42 | remoteStatus.tooltip = 'Click to disable remote database';
43 | }
44 | else {
45 | remoteStatus.text = '$(compass) PoiEx sync: $(circle-slash) Disabled';
46 | remoteStatus.tooltip = 'Click to enable remote database';
47 | }
48 | remoteStatus.show();
49 | }
50 |
51 | async function statusBarButtonPressed(context: vscode.ExtensionContext, rdb: RemoteDB, remoteStatus: vscode.StatusBarItem) {
52 | if (rdb.settingsEnabledAndConfigured()) {
53 | console.log("[IaC Main] Status bar button pressed: disabling remote database");
54 | await vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).update('collab.enabled', false, true);
55 | rdb.disable();
56 | updateStatusBar(rdb, remoteStatus);
57 | }
58 | else if (rdb.settingsConfigured()) {
59 | console.log("[IaC Main] Status bar button pressed: enabling remote database");
60 | await vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).update('collab.enabled', true, true);
61 | rdb.enable();
62 | updateStatusBar(rdb, remoteStatus);
63 | }
64 | else {
65 | console.log("[IaC Main] Status bar button pressed: opening settings page");
66 | // Open settings page
67 | vscode.commands.executeCommand('workbench.action.openSettings', `${constants.EXT_NAME}.collab`);
68 | }
69 | }
70 |
71 | export function initRemoteDB(context: vscode.ExtensionContext) {
72 | // Init status bar indicator for remote connection
73 | let remoteStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
74 |
75 | let rdb = new RemoteDB(context.secrets);
76 |
77 | // Register command for status bar button
78 | context.subscriptions.push(vscode.commands.registerCommand(`${constants.EXT_NAME}.statusBarButton`, () => {
79 | statusBarButtonPressed(context, rdb, remoteStatus);
80 | }));
81 |
82 | // Update status bar
83 | updateStatusBar(rdb, remoteStatus);
84 | rdb.onDisable(() => {
85 | // Update status bar
86 | updateStatusBar(rdb, remoteStatus);
87 | });
88 | rdb.onEnable(() => {
89 | // Update status bar
90 | updateStatusBar(rdb, remoteStatus);
91 | });
92 |
93 | rdb.onDbReady(() => {
94 | // Update status bar
95 | console.log("[IaC Main] Remote DB ready, updating status bar");
96 | updateStatusBar(rdb, remoteStatus);
97 | });
98 |
99 | return rdb;
100 | }
101 |
102 | // This method is called when your extension is activated
103 | // Your extension is activated the very first time the command is executed
104 | export function activate(context: vscode.ExtensionContext) {
105 | let storageUri = context.globalStorageUri;
106 | let iacPath = path.join(storageUri.fsPath, constants.IAC_FOLDER_NAME);
107 | console.log(iacPath);
108 |
109 | // Set context variable to hide / show sidebar views
110 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectOpen', false);
111 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectCreator', false);
112 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectEncrypted', false);
113 |
114 | // Check if number of workspace folders is 1
115 | if (vscode.workspace.workspaceFolders === undefined || vscode.workspace.workspaceFolders.length !== 1) {
116 | console.log('[IaC Main] Number of workspace folders is not 1, skipping activation');
117 | return;
118 | }
119 |
120 | // Ensure that the storage directory exists
121 | util.ensureDirExists(iacPath).then((res) => {
122 | if (res !== true) {
123 | console.log('[IaC Main] Could not create storage directory, skipping activation');
124 | return;
125 | }
126 | return init1(context, iacPath);
127 | });
128 |
129 | return;
130 | }
131 |
132 | async function init1(context: vscode.ExtensionContext, iacPath: string) {
133 | let iacUri = vscode.Uri.file(iacPath);
134 | pdb = new IaCProjectDir(context, iacUri);
135 | await pdb.init();
136 | let projectUuidList = (await pdb.listProjects())?.map((project: any) => project.uuid);
137 |
138 | // Check if a project is already open in the workspace
139 | let projectName = context.workspaceState.get('projectName', undefined);
140 | let projectUuid = context.workspaceState.get('projectUuid', undefined);
141 | if (!projectUuidList?.includes(projectUuid)) {
142 | projectUuid = undefined;
143 | projectName = undefined;
144 | }
145 |
146 | if (projectUuid !== undefined) {
147 | // Initialize remote database
148 | if (rdb === undefined) {
149 | rdb = initRemoteDB(context);
150 | }
151 |
152 | console.log('[IaC Main] Project already open in workspace');
153 | openProject(context, iacUri, projectUuid);
154 | }
155 |
156 | let ensureDbOk = () => {
157 | // Initialize remote database
158 | if (rdb === undefined) {
159 | rdb = initRemoteDB(context);
160 | }
161 |
162 | if (rdb.settingsEnabled() && !rdb.settingsConfigured()) {
163 | // Show notification
164 | vscode.window.showInformationMessage('Please configure the remote database settings first, or disable remote database.');
165 |
166 | // Open settings page
167 | vscode.commands.executeCommand('workbench.action.openSettings', `${constants.EXT_NAME}.collab`);
168 | return false;
169 | }
170 | return true;
171 | };
172 |
173 | // No project open
174 | // Register command to initialize project
175 | console.log(`[IaC Main] Registering ${constants.EXT_NAME}.initProject command`);
176 | context.subscriptions.push(vscode.commands.registerCommand(`${constants.EXT_NAME}.initProject`, () => {
177 | console.log("[IaC Main] Init project button pressed");
178 | if (!ensureDbOk()) {
179 | console.log('[IaC Main] Remote database not configured, cannot initialize project');
180 | return;
181 | }
182 |
183 | // Ask for project name
184 | vscode.window.showInputBox({
185 | placeHolder: 'Please enter project name',
186 | prompt: 'Please enter project name',
187 | validateInput: (value: string) => {
188 | if (value === undefined || value === '') {
189 | return 'Project name cannot be empty';
190 | }
191 | return undefined;
192 | }
193 | }).then((projectName) => {
194 | if (projectName === undefined) {
195 | return;
196 | }
197 | // Quickselect "Do you want to encrypt the project?" dialog
198 | vscode.window.showQuickPick(['Yes', 'No'], {
199 | placeHolder: 'Do you want to encrypt the project?'
200 | }).then(async (value) => {
201 | if (value === undefined) {
202 | return;
203 | }
204 | let encrypt = value === 'Yes';
205 | // Push to local database
206 | let projectUuid = util.genUUID();
207 | let projectSecret = encrypt ? await IaCEncryption.genKey() : null;
208 | await pdb.pushProject(projectUuid, projectName, projectSecret);
209 | pdb.safeSyncProjects(rdb);
210 |
211 | openProject(context, iacUri, projectUuid);
212 | });
213 | });
214 | }));
215 |
216 | // Register command to open an existing project
217 | context.subscriptions.push(vscode.commands.registerCommand(`${constants.EXT_NAME}.openProject`, async (projectUuid: string | undefined = undefined) => {
218 | console.log('[IaC Main] Open project button pressed');
219 | if (!ensureDbOk()) {
220 | console.log('[IaC Main] Remote database not configured, cannot open project');
221 | return;
222 | }
223 |
224 | let cb2 = async (choice: string | undefined) => {
225 | if (choice === undefined) {
226 | return;
227 | }
228 | let projectUuid = choice;
229 | if (choice.includes(' $ ')) {
230 | projectUuid = choice.split(' $ ')[1];
231 | }
232 | let project = await pdb.getProject(projectUuid);
233 | assert(project !== null, "Project not found in local database");
234 | if (project === null) { return; };
235 | if (project[3] === null || project[2] !== null) {
236 | openProject(context, iacUri, projectUuid);
237 | return;
238 | }
239 | // Ask for project secret
240 | vscode.window.showInputBox({
241 | placeHolder: 'Please enter project secret',
242 | prompt: 'Please enter project secret',
243 | password: true,
244 | validateInput: (value: string) => {
245 | if (value === undefined || value === '') {
246 | return 'Project secret cannot be empty';
247 | }
248 | return undefined;
249 | }
250 | }).then((projectSecret) => {
251 | if (projectSecret === undefined) {
252 | return;
253 | }
254 | openProject(context, iacUri, projectUuid, projectSecret);
255 | });
256 | };
257 |
258 | let cb = async () => {
259 | console.log('[IaC Main] Open project ready, executing callback');
260 | if (projectUuid !== undefined) {
261 | cb2(projectUuid);
262 | return;
263 | }
264 |
265 | // Show list of projects as quickpick
266 | let projectList = (await pdb.listProjects()) as {}[];
267 | projectTreeView.update(projectList);
268 | let projectNames = projectList.map((project: any) => project.name + " $ " + project.uuid);
269 | console.log('[IaC Main] Open project got list of projects');
270 | if (projectNames.length === 0) {
271 | vscode.window.showInformationMessage('No projects found, please create a new project first.');
272 | return;
273 | }
274 |
275 | vscode.window.showQuickPick(projectNames, {
276 | placeHolder: 'Please select a project to open'
277 | }).then(cb2);
278 | };
279 |
280 | if (rdb.settingsEnabled()) {
281 | rdb.onDbReadyOnce(async () => { await pdb.safeSyncProjects(rdb); await cb(); });
282 | }
283 | else {
284 | await cb();
285 | }
286 | }));
287 |
288 | // Experimental tree view
289 | if (ensureDbOk()) {
290 | projectTreeView = new tree.ProjectTreeViewManager(context, pdb, rdb);
291 | context.subscriptions.push(projectTreeView);
292 | projectTreeView.show();
293 | projectTreeView.update();
294 | }
295 | else {
296 | projectTreeView = new tree.ProjectTreeViewManager(context, pdb, undefined);
297 | context.subscriptions.push(projectTreeView);
298 | projectTreeView.showDbError();
299 | console.log('[IaC Main] Remote database not configured, cannot show project list');
300 | }
301 | }
302 |
303 | async function openProject(context: vscode.ExtensionContext, storageUri: vscode.Uri, projectUuid: string, projectSecret: string | null = null) {
304 | let projectAttrs = await pdb.getProject(projectUuid);
305 | assert(projectAttrs !== null, "Project not found in local database (2)");
306 | if (projectAttrs === null) {
307 | return;
308 | }
309 | let [puuid, projectName, pkeys, jwt] = projectAttrs;
310 |
311 | let mIaCEncryption = new IaCEncryption();
312 | if (projectSecret === null) {
313 | projectSecret = pkeys;
314 | }
315 | await mIaCEncryption.setKey(projectSecret);
316 | if (((pkeys === "") || (pkeys === null)) && (jwt !== "") && (jwt !== null)) { // If no key was in database and project uses encryption
317 | // Check that key is correct and add to database
318 | if (await mIaCEncryption.checkKey(jwt) !== puuid) {
319 | vscode.window.showErrorMessage('Incorrect project secret');
320 | mIaCEncryption.dispose();
321 | return;
322 | }
323 | // Save key to database
324 | await pdb.pushProject(puuid, projectName, projectSecret);
325 | }
326 | projectDisposables.push(mIaCEncryption);
327 | // If key is in database, assume it's correct
328 |
329 | // Set workspaceState variables to remember project
330 | context.workspaceState.update('projectName', projectName);
331 | context.workspaceState.update('projectUuid', projectUuid);
332 | context.workspaceState.update('projectEncrypted', projectSecret !== null);
333 |
334 | // Set context variables to show / hide sidebar views
335 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectOpen', true);
336 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectCreator', true);
337 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectEncrypted', projectSecret !== null);
338 |
339 | // Hide project tree view
340 | projectTreeView.hide();
341 |
342 | rdb.setProjectUuid(projectUuid, projectSecret);
343 |
344 | await initLocalDb(storageUri.fsPath, projectUuid);
345 |
346 | // Continue with extension initialization
347 | mComments = new comments.IaCComments(context, db, rdb);
348 |
349 | // Init diagnostics
350 | let mIaCDiagnostics = new IaCDiagnostics(context, db, rdb);
351 | mIaCDiagnostics.loadDiagnostics(); // Do not await promise
352 |
353 | // Init IaC webview manager
354 | let mIacWebviewManager = new IaCWebviewManager(context, mIaCDiagnostics);
355 |
356 | // Commands to manually load Semgrep results
357 | let disposableCommand1 = vscode.commands.registerCommand(`${constants.EXT_NAME}.readSemgrepJson`, () => handleOpenSemgrepJson(context, mIaCDiagnostics));
358 | context.subscriptions.push(disposableCommand1); projectDisposables.push(disposableCommand1);
359 |
360 | let disposableCommand3 = vscode.commands.registerCommand(`${constants.EXT_NAME}.deleteAllDiagnostics`, async () => await mIaCDiagnostics.clearDiagnostics());
361 | context.subscriptions.push(disposableCommand3); projectDisposables.push(disposableCommand3);
362 |
363 | let disposableCommand4 = vscode.commands.registerCommand(`${constants.EXT_NAME}.runSemgrep`, () => handleStartSemgrepScan(context, mIaCDiagnostics));
364 | context.subscriptions.push(disposableCommand4); projectDisposables.push(disposableCommand4);
365 |
366 | // Generic command to open an arbitrary link
367 | let disponsableCommand5 = vscode.commands.registerCommand(`${constants.EXT_NAME}.openLink`, (link: string) => {
368 | vscode.env.openExternal(vscode.Uri.parse(link));
369 | });
370 | context.subscriptions.push(disponsableCommand5); projectDisposables.push(disponsableCommand5);
371 |
372 | // Register command to close current project
373 | let disposableCommand6 = vscode.commands.registerCommand(`${constants.EXT_NAME}.closeProject`, async () => {
374 | await closeProject(context, projectUuid, db, mIaCDiagnostics, mComments, mIacWebviewManager);
375 | });
376 | context.subscriptions.push(disposableCommand6); projectDisposables.push(disposableCommand6);
377 |
378 | // Register command to export current project
379 | let disposableCommand7 = vscode.commands.registerCommand(`${constants.EXT_NAME}.exportProject`, () => {
380 | let saveUri = vscode.Uri.file(`${projectName}.sqlite3`);
381 | vscode.window.showSaveDialog({ title: "Export to file", saveLabel: "Export project", defaultUri: saveUri }).then(fileInfos => {
382 | if (fileInfos === undefined) {
383 | return;
384 | }
385 | fs.copyFileSync(db.path, fileInfos.path);
386 | });
387 | });
388 | context.subscriptions.push(disposableCommand7); projectDisposables.push(disposableCommand7);
389 |
390 | // Register command to destroy current project
391 | let disposableCommand8 = vscode.commands.registerCommand(`${constants.EXT_NAME}.destroyProject`, async () => {
392 | vscode.window.showWarningMessage('Are you sure you want to destroy this project (remove also on remote)?', 'Yes', 'No').then(async (choice) => {
393 | if (await pdb.getProject(projectUuid) === null) { return; }
394 | if (choice === 'Yes') {
395 | await pdb.removeProject(projectUuid, rdb);
396 | await closeProject(context, projectUuid, db, mIaCDiagnostics, mComments, mIacWebviewManager);
397 | }
398 | });
399 | });
400 | context.subscriptions.push(disposableCommand8); projectDisposables.push(disposableCommand8);
401 | let disposableCommand9 = vscode.commands.registerCommand(`${constants.EXT_NAME}.destroyLocalProject`, async () => {
402 | vscode.window.showWarningMessage('Are you sure you want to destroy this project (remove only locally)?', 'Yes', 'No').then(async (choice) => {
403 | if (await pdb.getProject(projectUuid) === null) { return; }
404 | if (choice === 'Yes') {
405 | await pdb.removeProject(projectUuid, null);
406 | await closeProject(context, projectUuid, db, mIaCDiagnostics, mComments, mIacWebviewManager);
407 | }
408 | });
409 | });
410 | context.subscriptions.push(disposableCommand9); projectDisposables.push(disposableCommand9);
411 |
412 | // Register command to copy secret to the user's clipboard
413 | let disposableCommand10 = vscode.commands.registerCommand(`${constants.EXT_NAME}.copyKey`, () => {
414 | assert(projectSecret !== null, "Pressed copyKey on an unencrypted project");
415 | if (projectSecret === null) { return; }
416 |
417 | // Copy to clipboard and Notify user
418 | vscode.env.clipboard.writeText(projectSecret);
419 | vscode.window.showInformationMessage('Copied project secret to clipboard');
420 | });
421 | context.subscriptions.push(disposableCommand10); projectDisposables.push(disposableCommand10);
422 | }
423 |
424 | async function closeProject(context: vscode.ExtensionContext, projectUuid: string, db: LocalDB, mIaCDiagnostics: IaCDiagnostics, mComments: comments.IaCComments, mIacWebviewManager: IaCWebviewManager) {
425 | // Prevent race conditions
426 | if (projectClosing) {
427 | console.log("[IaC Main] closeProject(): Project is already closing");
428 | return;
429 | }
430 | projectClosing = true;
431 |
432 | await mComments.dispose();
433 | mIacWebviewManager.dispose();
434 | mIaCDiagnostics.dispose();
435 |
436 | // Dispose all project disposables
437 | projectDisposables.forEach((disposable) => {
438 | context.subscriptions.splice(context.subscriptions.indexOf(disposable), 1);
439 | disposable.dispose();
440 | });
441 |
442 | rdb.setProjectUuid(null);
443 | console.log("[IaC Main] closeProject(): Closing project " + projectUuid);
444 | await db.close();
445 |
446 | // Clear workspaceState variables
447 | context.workspaceState.update('projectName', undefined);
448 | context.workspaceState.update('projectUuid', undefined);
449 | context.workspaceState.update('projectEncrypted', undefined);
450 |
451 | // Set context variables to show / hide sidebar views
452 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectOpen', false);
453 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectCreator', false);
454 | vscode.commands.executeCommand('setContext', 'iacAudit.isProjectEncrypted', false);
455 |
456 | // Show project tree view
457 | projectTreeView.show().then(() => {
458 | projectTreeView.update();
459 | });
460 |
461 | // Race condition prevention
462 | projectClosing = false;
463 | }
464 |
465 | // This method is called when your extension is deactivated
466 | export function deactivate() {
467 | db.close();
468 | // All disposables are automatically disposed when extension is deactivated
469 | }
--------------------------------------------------------------------------------
/src/iacview.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as which from 'which';
3 | import * as child_process from "child_process";
4 | import { assert } from 'console';
5 | import * as path from 'path';
6 | import { Uri } from "vscode";
7 | import * as fs from 'fs';
8 | import * as ssri from 'ssri';
9 | import { getApi, FileDownloader } from "@microsoft/vscode-file-downloader-api";
10 | import * as tar from 'tar';
11 | import * as os from 'os';
12 |
13 | import { IaCDiagnostics } from './diagnostics';
14 | import * as constants from './constants';
15 | import * as util from './util';
16 | import { IaCPoIViewManager } from './poiview';
17 | import { runSemgrepHcl } from './semgrep';
18 |
19 |
20 | export class IaCWebviewManager {
21 | private context: vscode.ExtensionContext;
22 | private mIaCDiagnostics: IaCDiagnostics;
23 | private disposables: vscode.Disposable[] = [];
24 | private currentPanel: vscode.WebviewPanel | undefined = undefined;
25 | private diagnostics: Map = new Map();
26 | private disposed = false;
27 | private hideProgressBar: any = undefined;
28 | private inframapDownloading = false;
29 | private diagram: string | undefined = undefined;
30 | private resourceBlocks: Map = new Map;
31 |
32 | constructor(context: vscode.ExtensionContext, mIaCDiagnostics: IaCDiagnostics) {
33 | this.context = context;
34 | this.mIaCDiagnostics = mIaCDiagnostics;
35 |
36 | let disposableCommand1 = vscode.commands.registerCommand('poiex.showIaCwebview', () => {
37 | const options: vscode.OpenDialogOptions = {
38 | canSelectMany: false,
39 | openLabel: 'Select IaC definiton file',
40 | filters: {
41 | 'Terraform files': ['tf'],
42 | 'All files': ['*']
43 | }
44 | };
45 |
46 | vscode.window.showOpenDialog(options).then(
47 | fileUri => {
48 | if (!(fileUri && fileUri[0])) {
49 | return;
50 | }
51 | if (this.diagnostics.size > 0) {
52 | return fileUri;
53 | }
54 | return vscode.window.showInformationMessage("No findings. Do you want to run Semgrep?", "Yes", "No").then(
55 | async (ans) => {
56 | if (ans == "No") {
57 | return fileUri;
58 | }
59 | await util.handleStartSemgrepScan(context, mIaCDiagnostics);
60 | return fileUri;
61 | }
62 | );
63 | }
64 | ).then(fileUri => {
65 | if (fileUri === undefined) return;
66 | if (fileUri === null) return;
67 | console.log('Selected file: ' + fileUri[0].fsPath);
68 |
69 | this.runInframap(fileUri[0].fsPath);
70 | });
71 | });
72 | context.subscriptions.push(disposableCommand1);
73 | this.disposables.push(disposableCommand1);
74 | mIaCDiagnostics.onDiagnosticsUpdate((diagnostics) => { this.diagnosticsUpdate(diagnostics); });
75 | }
76 |
77 | dispose() {
78 | this.disposed = true;
79 | for (let d of this.disposables) {
80 | this.context.subscriptions.splice(this.context.subscriptions.indexOf(d), 1);
81 | d.dispose();
82 | }
83 | if (this.currentPanel !== undefined) { this.currentPanel.dispose(); }
84 | }
85 |
86 | async downloadInframap(): Promise {
87 | assert(this.inframapDownloading === false);
88 | if (this.inframapDownloading) { return undefined; }
89 |
90 | let inframapUrl = constants.INFRAMAP_RELEASES[process.platform].url;
91 | if (inframapUrl === undefined) {
92 | // Show notification to user
93 | vscode.window.showInformationMessage(
94 | `Unable to install Inframap for platform ${process.platform}. Please install Inframap to use ${constants.EXT_COMMON_NAME}.`,
95 | );
96 | return undefined;
97 | }
98 |
99 | this.inframapDownloading = true;
100 | const fileDownloader: FileDownloader = await getApi();
101 |
102 |
103 | const file: Uri = await vscode.window.withProgress({
104 | location: vscode.ProgressLocation.Notification,
105 | title: "Downloading Inframap...",
106 | cancellable: false
107 | }, async (progress, token) => {
108 | return await fileDownloader.downloadFile(
109 | Uri.parse(inframapUrl),
110 | `${util.genUUID()}.tar.gz`,
111 | this.context,
112 | undefined,
113 | (downloadedBytes: number | undefined, totalBytes: number | undefined) => {
114 | if (downloadedBytes === undefined || totalBytes === undefined) {
115 | return;
116 | }
117 | progress.report({ increment: (downloadedBytes / totalBytes) * 100 });
118 | }
119 | );
120 | });
121 |
122 | console.log(file);
123 | return file;
124 | }
125 |
126 | async inframapIntegrityCheck(inframapPath: string): Promise {
127 | if (typeof inframapPath !== "string") { return false; }
128 | let integrity = constants.INFRAMAP_RELEASES[process.platform].integrity;
129 | try {
130 | let sri = await ssri.checkStream(fs.createReadStream(inframapPath), integrity);
131 | console.log(`[IaC View] Inframap integrity check passed: ${sri}`);
132 | return true;
133 | } catch (e) {
134 | console.log(`[IaC View] Inframap integrity check failed for ${inframapPath}`);
135 | return false;
136 | }
137 | }
138 |
139 | async getInframapPath(): Promise {
140 | if (this.inframapDownloading) {
141 | // No need to alert the user, there is already a progress notification shown
142 | return undefined;
143 | }
144 |
145 | let pathSetting: string | undefined = vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).get('inframapPath');
146 | if ((pathSetting !== undefined) && (pathSetting !== "")) {
147 | let res = which.sync(pathSetting, { nothrow: true });
148 | if ((res === undefined) || (res === null)) {
149 | const resp = vscode.window.showInformationMessage(
150 | `Inframap not installed. Please install Inframap to use ${constants.EXT_COMMON_NAME}, or set Inframap path to empty.`,
151 | );
152 | } else {
153 | return res;
154 | }
155 | }
156 |
157 | let inframapDownloaded = this.context.globalState.get(constants.INFRAMAP_DOWNLOADED_STATENAME, undefined) !== undefined;
158 | if (inframapDownloaded) {
159 | let inframapPath: string | undefined = this.context.globalState.get(constants.INFRAMAP_DOWNLOADED_STATENAME, undefined);
160 | if ((inframapPath !== undefined) && (await this.inframapIntegrityCheck(inframapPath))) {
161 | return inframapPath;
162 | }
163 | }
164 |
165 | console.log(`[IaC View] Inframap not found, downloading...`);
166 | let res = await this.downloadInframap();
167 | if ((res === undefined) || (!await this.inframapIntegrityCheck(res.fsPath))) {
168 | vscode.window.showInformationMessage(
169 | `An error occurred while downloading Inframap. Please install Inframap to use ${constants.EXT_COMMON_NAME}.`,
170 | );
171 | return undefined;
172 | }
173 | this.context.globalState.update(constants.INFRAMAP_DOWNLOADED_STATENAME, res.fsPath);
174 | return res.fsPath;
175 | }
176 |
177 | async inframapCallback(error: any, stdout: any, stderr: any, semgrepPromise: Promise): Promise {
178 | let semgrepOutputJson = await semgrepPromise;
179 |
180 | if (this.hideProgressBar) {
181 | this.hideProgressBar();
182 | this.hideProgressBar = undefined;
183 | }
184 |
185 | if (semgrepOutputJson === null) {
186 | console.log(`[IaC View] semgrep output is null`);
187 | vscode.window.showErrorMessage(`Inframap error: Semgrep output is null`);
188 | return;
189 | }
190 |
191 | let semgrepOutput: any;
192 | try {
193 | semgrepOutput = JSON.parse(semgrepOutputJson);
194 | } catch (e) {
195 | console.log(`[IaC View] semgrep output is not JSON: ${semgrepOutputJson}`);
196 | vscode.window.showErrorMessage(`Inframap error: Semgrep output is not JSON`);
197 | return;
198 | }
199 | if (semgrepOutput === undefined) { assert(false, "semgrepOutputJson undefined"); return; }
200 | if (semgrepOutput.results === undefined) { assert(false, "semgrepOutputJson.results undefined"); return; }
201 | console.log(`[IaC View] Semgrep HCL parse found ${semgrepOutput.results.length} resource blocks`);
202 |
203 | // Convert results to a data structure. Map metavars $RT $RN to key = $RT.$RN Value = start line:start col, end line:end col
204 | let resourceBlocks: Map = new Map();
205 | for (let result of semgrepOutput.results) {
206 | try {
207 | let startLine = result.start.line;
208 | let startCol = result.start.col;
209 | let endLine = result.end.line;
210 | let endCol = result.end.col;
211 | let filePath = result.path;
212 | let resourceName = result.extra.metavars.$RN.abstract_content;
213 | let resourceType = result.extra.metavars.$RT.abstract_content;
214 | let key = `${resourceType}.${resourceName}`;
215 | resourceBlocks.set(key, [filePath, startLine, startCol, endLine, endCol]);
216 | } catch (e) {
217 | console.log(`[IaC View] error: ${e}`);
218 | }
219 | }
220 | this.resourceBlocks = resourceBlocks;
221 |
222 | if (error) {
223 | console.log(`[IaC View] error: ${error.message}`);
224 | let timeoutFormatted = (constants.INFRAMAP_TIMEOUT_MS / 1000).toFixed(2);
225 | let msg = `Inframap timeout (${timeoutFormatted}s) exceeded or execution error. Error: ${error.message}`;
226 | vscode.window.showErrorMessage(msg);
227 | return;
228 | }
229 | if (stderr) {
230 | console.log(`[IaC View] stderr: ${stderr}`);
231 | vscode.window.showErrorMessage(`Inframap error: ${stderr}`);
232 | return;
233 | }
234 | console.log(`[IaC View] Inframap done, stdout: ${stdout}`);
235 | this.diagram = stdout;
236 | assert(this.diagram !== undefined);
237 | if (this.diagram === undefined) { return; }
238 |
239 | let localResourceRoots: Set | vscode.Uri[] = new Set();
240 | let imagePaths = this.diagram?.matchAll(/image=\"(.*?)\"/g);
241 | for (let match of imagePaths) {
242 | let imagePath = match[1];
243 | let imageUri = vscode.Uri.file(path.dirname(imagePath));
244 | localResourceRoots.add(imageUri);
245 | }
246 | localResourceRoots.add(vscode.Uri.joinPath(this.context.extensionUri, 'res/'));
247 | localResourceRoots = Array.from(localResourceRoots);
248 | console.log(`[IaC View] localResourceRoots: ${localResourceRoots}`);
249 |
250 | this.currentPanel = vscode.window.createWebviewPanel(
251 | 'iacDiagram',
252 | 'IaC Analysis Diagram',
253 | vscode.ViewColumn.One, // Editor column to show the new webview panel in.
254 | { enableScripts: true, localResourceRoots: localResourceRoots } // Webview options.
255 | );
256 |
257 | this.currentPanel.onDidDispose(() => { this.currentPanel = undefined; }, null, this.disposables);
258 |
259 | this.currentPanel.webview.html = this.getWebviewContent();
260 |
261 | this.currentPanel.webview.onDidReceiveMessage(message => {
262 | switch (message.command) {
263 | case 'nodeClicked':
264 | // Sanitize message.nodeId
265 | if (message.nodeId === undefined) { return; }
266 | assert(this.resourceBlocks !== undefined);
267 | if (this.resourceBlocks === undefined) { return; }
268 | let serviceName = message.nodeId.replace(/[^0-9a-zA-Z_\.\-]/g, "");
269 | new IaCPoIViewManager(this.context, this.mIaCDiagnostics, serviceName, this.resourceBlocks);
270 | return;
271 | }
272 | });
273 | }
274 |
275 | async runInframap(codePath: string): Promise {
276 | let inframapPath = await this.getInframapPath();
277 | if (inframapPath === undefined) { return; }
278 |
279 | const filenames: string[] = [];
280 | await tar.t({
281 | file: inframapPath,
282 | onentry: entry => filenames.push(entry.path),
283 | });
284 | if (filenames.length === 0) { return; };
285 | let inframapExecName = filenames[0];
286 |
287 | // Create a temporary directory
288 | const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'inframap-'));
289 |
290 | await tar.x(
291 | {
292 | file: inframapPath,
293 | cwd: tmpDir
294 | },
295 | [inframapExecName]
296 | );
297 | inframapPath = path.join(tmpDir, inframapExecName);
298 |
299 | if (this.hideProgressBar) {
300 | vscode.window.showErrorMessage("Inframap is already running.");
301 | return;
302 | }
303 |
304 | vscode.window.withProgress({
305 | location: { viewId: 'iacAudit' }
306 | }, (progress, token) => {
307 | return new Promise((resolve, reject) => {
308 | this.hideProgressBar = resolve;
309 | });
310 | });
311 |
312 | let codeDir = path.dirname(codePath);
313 | let inframapArgs = "generate --clean=false --hcl";
314 | let inframapArgsArray = inframapArgs.split(" ").concat(codeDir);
315 | console.log(`[IaC View] Running Inframap (${inframapPath}) with args: ${inframapArgsArray}`);
316 |
317 | // Start a Semgrep run to parse HCL files
318 | let semgrepPromise = util.handleStartSemgrepScan(this.context, null, codeDir);
319 |
320 | try {
321 | child_process.execFile(inframapPath,
322 | inframapArgsArray,
323 | { timeout: constants.INFRAMAP_TIMEOUT_MS },
324 | (error, stdout, stderr) => {
325 | this.inframapCallback(error, stdout, stderr, semgrepPromise);
326 | }
327 | );
328 | } catch (error) {
329 | const resp = vscode.window.showInformationMessage(
330 | `Error while running Inframap.`,
331 | );
332 | return;
333 | }
334 | return;
335 | }
336 |
337 |
338 | diagnosticsUpdate(diagnostics: Map) {
339 | if (this.disposed) { return; }
340 | this.diagnostics = diagnostics;
341 | if (this.currentPanel !== undefined) {
342 | this.currentPanel.webview.html = this.getWebviewContent();
343 | }
344 | }
345 |
346 | private getWebviewContent(): string {
347 | let diagram = "";
348 | if (this.diagram !== undefined) {
349 | diagram = this.diagram;
350 | }
351 | if ((this.currentPanel !== undefined) && (this.currentPanel.webview !== undefined)) {
352 | diagram = diagram.replace(/image=\"(.*?)\"/g, (match, capture) => {
353 | if (this.currentPanel === undefined) { return match; }
354 | return `image="${this.currentPanel.webview.asWebviewUri(vscode.Uri.file(capture))}"`;
355 | });
356 | }
357 | let formattedDiagram = diagram.replace(/\'/g, '').replace(/\n/g, '');
358 | console.log(`[IaC View] Diagram: ${diagram}`);
359 |
360 | // For each resource node, count how many PoIs there are
361 | let poiList = [];
362 | for (let [key, value] of this.diagnostics) {
363 | if (!value.message.includes(constants.IAC_POI_MESSAGE)) { continue; };
364 | poiList.push(value.message);
365 | }
366 |
367 | // For each resource node, count how many PoIs there are
368 | let findingsList = [];
369 | for (let [poiFilter, resourceBlock] of this.resourceBlocks) {
370 | let [path, startLine, startCol, endLine, endCol] = resourceBlock;
371 | // Semgrep lines are 1-indexed, vscode lines are 0-indexed
372 | startLine = startLine - 1;
373 | endLine = endLine - 1;
374 | for (let [key, value] of this.diagnostics) {
375 | if (value.path !== path) { continue; };
376 | if ((value.line > endLine) || (value.line < startLine)) { continue; }
377 | findingsList.push(poiFilter);
378 | }
379 | }
380 |
381 | let scriptUri = vscode.Uri.joinPath(this.context.extensionUri, 'res', 'iacView.js');
382 | let styleUri = vscode.Uri.joinPath(this.context.extensionUri, 'res', 'iacView.css');
383 | let scriptWebUri = "./res/iacView.js";
384 | let styleWebUri = "./res/iacView.css";
385 | if (this.currentPanel !== undefined) {
386 | scriptWebUri = this.currentPanel.webview.asWebviewUri(scriptUri).toString();
387 | styleWebUri = this.currentPanel.webview.asWebviewUri(styleUri).toString();
388 | }
389 |
390 | let res = `
391 |
392 |
393 |
394 |
395 |
402 |
403 |
404 |
405 |
406 |
407 |
408 | `;
409 | return res;
410 | }
411 | }
--------------------------------------------------------------------------------
/src/poiview.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 |
3 | import { IaCDiagnostics } from './diagnostics';
4 | import * as constants from './constants';
5 |
6 |
7 | export class IaCPoIViewManager {
8 | private context: vscode.ExtensionContext;
9 | private disposables: vscode.Disposable[] = [];
10 | private currentPanel: vscode.WebviewPanel | undefined = undefined;
11 | private diagnostics: Map = new Map();
12 | private disposed = false;
13 | private poiFilter;
14 | private mIaCDiagnostics: IaCDiagnostics;
15 | private resourceBlocks: Map;
16 |
17 | constructor(context: vscode.ExtensionContext, mIaCDiagnostics: IaCDiagnostics, poiFilter: string, resourceBlocks: Map) {
18 | this.context = context;
19 | this.poiFilter = poiFilter;
20 | this.resourceBlocks = resourceBlocks;
21 | this.mIaCDiagnostics = mIaCDiagnostics;
22 | mIaCDiagnostics.onDiagnosticsUpdate((diagnostics) => { this.diagnosticsUpdate(diagnostics); });
23 |
24 | this.currentPanel = vscode.window.createWebviewPanel(
25 | 'poiView',
26 | `IaC PoI (${poiFilter})`,
27 | vscode.ViewColumn.One, // Editor column to show the new webview panel in.
28 | { enableScripts: true } // Webview options.
29 | );
30 |
31 | this.currentPanel.onDidDispose(() => { this.currentPanel = undefined; }, null, this.disposables);
32 |
33 | this.getWebviewContent().then( data => {
34 | if (this.currentPanel !== undefined) {
35 | this.currentPanel.webview.html = data;
36 | }
37 | })
38 |
39 | this.currentPanel.webview.onDidReceiveMessage(async message => {
40 | console.log(message);
41 | switch (message.command) {
42 | case 'openPoi':
43 | let poiUUID = message.poiUUID;
44 | let poi = this.diagnostics.get(poiUUID);
45 | console.log(poi);
46 | if (poi === undefined) {
47 | vscode.window.showErrorMessage(`PoI ${poiUUID} not found`);
48 | return;
49 | }
50 |
51 | // Go to the file and line
52 | let doc = await mIaCDiagnostics.getDocumentFromSemgrepPath(poi.path);
53 | if (doc === undefined) {
54 | vscode.window.showErrorMessage(`File ${poi.path} not found`);
55 | return;
56 | }
57 | let line = poi.line;
58 | vscode.window.showTextDocument(doc).then((editor) => {
59 | let range = new vscode.Range(line, 0, line, 100);
60 | editor.selection = new vscode.Selection(range.start, range.end);
61 | editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
62 | });
63 | break;
64 | case 'openIaC':
65 | let resourceBlock = this.resourceBlocks.get(this.poiFilter);
66 | if (resourceBlock === undefined) {
67 | vscode.window.showErrorMessage(`No resource block found for ${this.poiFilter}`);
68 | return;
69 | }
70 | let [path, startLine, startCol, endLine, endCol] = resourceBlock;
71 | let doc2 = await mIaCDiagnostics.getDocumentFromSemgrepPath(path);
72 | if (doc2 === undefined) {
73 | vscode.window.showErrorMessage(`File ${path} not found`);
74 | return;
75 | }
76 | let range = new vscode.Range(startLine - 1, startCol - 1, endLine - 1, endCol - 1);
77 | vscode.window.showTextDocument(doc2).then((editor) => {
78 | editor.selection = new vscode.Selection(range.start, range.end);
79 | editor.revealRange(range, vscode.TextEditorRevealType.InCenter);
80 | });
81 | break;
82 | default:
83 | console.error(`[PoI View] Unknown command ${message.command}`);
84 | break;
85 | }
86 | });
87 | }
88 |
89 | dispose() {
90 | this.disposed = true;
91 | for (let d of this.disposables) {
92 | this.context.subscriptions.splice(this.context.subscriptions.indexOf(d), 1);
93 | d.dispose();
94 | }
95 | if (this.currentPanel !== undefined) { this.currentPanel.dispose(); }
96 | }
97 |
98 | diagnosticsUpdate(diagnostics: Map) {
99 | if (this.disposed) { return; }
100 | this.diagnostics = diagnostics;
101 | this.getWebviewContent().then( data => {
102 | if (this.currentPanel !== undefined) {
103 | this.currentPanel.webview.html = data;
104 | }
105 | })
106 | }
107 |
108 | private async getWebviewContent(): Promise {
109 | let poiList = [];
110 | let providerFilter = "";
111 | let serviceFilter = "";
112 | let poiFilterParts = this.poiFilter.split("_");
113 | if (poiFilterParts.length >= 1) {
114 | providerFilter = this.poiFilter.split("_")[0];
115 | }
116 | if (poiFilterParts.length >= 2) {
117 | serviceFilter = this.poiFilter.split("_")[1];
118 | }
119 |
120 | // Filter Points of Intersections that are related to the selected cloud service
121 | for (let [key, value] of this.diagnostics) {
122 | if (!value.message.includes(constants.IAC_POI_MESSAGE)) { continue; };
123 | if (!value.message.toLowerCase().includes(providerFilter.toLowerCase())) { continue; }
124 | if (!value.message.toLowerCase().includes(serviceFilter.toLowerCase())) { continue; }
125 | poiList.push([key, value.message, value.path, value.line]);
126 | }
127 | // Filter diagnostics that match the currently selected resource block
128 | let resourceBlock = this.resourceBlocks.get(this.poiFilter);
129 | if (resourceBlock !== undefined) {
130 | let [path, startLine, startCol, endLine, endCol] = resourceBlock;
131 | // Semgrep lines are 1-indexed, vscode lines are 0-indexed
132 | startLine = startLine - 1;
133 | endLine = endLine - 1;
134 | for (let [key, value] of this.diagnostics) {
135 | if (value.path !== path) { continue; };
136 | if ((value.line > endLine) || (value.line < startLine)) { continue; }
137 | poiList.push([key, value.message, value.path, value.line]);
138 | }
139 | console.log(`[PoI View] Resource block found for ${this.poiFilter}`);
140 | }
141 | else {
142 | console.log(`[PoI View] No resource block found for ${this.poiFilter}`);
143 | }
144 |
145 | // Get IaC definition for selected resource block
146 | let iacDefinitionBlock: string | null = null;
147 | if (resourceBlock !== undefined) {
148 | let [path, startLine, startCol, endLine, endCol] = resourceBlock;
149 |
150 | // Document from semgrep path
151 | let doc = await this.mIaCDiagnostics.getDocumentFromSemgrepPath(path);
152 | if (doc === undefined) {
153 | vscode.window.showErrorMessage(`File ${path} not found`);
154 | }
155 | else {
156 | // Semgrep lines are 1-indexed, vscode lines are 0-indexed
157 | let r = new vscode.Range(startLine - 1, startCol - 1, endLine - 1, endCol - 1);
158 | iacDefinitionBlock = doc.getText(r);
159 | }
160 | }
161 |
162 | let scriptUri = vscode.Uri.joinPath(this.context.extensionUri, 'res', 'poiView.js');
163 | let styleUri = vscode.Uri.joinPath(this.context.extensionUri, 'res', 'poiView.css');
164 | let scriptWebUri = "./res/poiView.js";
165 | let styleWebUri = "./res/poiView.css";
166 | if (this.currentPanel !== undefined) {
167 | scriptWebUri = this.currentPanel.webview.asWebviewUri(scriptUri).toString();
168 | styleWebUri = this.currentPanel.webview.asWebviewUri(styleUri).toString();
169 | }
170 |
171 | let res = `
172 |
173 |
174 |
175 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | `;
188 | return res;
189 | }
190 | }
--------------------------------------------------------------------------------
/src/projects.ts:
--------------------------------------------------------------------------------
1 | // IaC Project directory
2 |
3 | import * as path from "path";
4 | import * as vscode from "vscode";
5 | import * as sqlite3 from 'sqlite3';
6 | import { open, Database } from 'sqlite';
7 | import assert = require("assert");
8 |
9 | import { RemoteDB } from "./remote";
10 | import { LocalDB } from "./db";
11 | import { IaCEncryption } from "./encryption";
12 | import * as constants from "./constants";
13 |
14 | export class IaCProjectDir {
15 | private pdb: Database | null;
16 | private context: vscode.ExtensionContext;
17 | private currentSync: Promise | undefined = undefined;
18 | private iacPath: vscode.Uri;
19 |
20 | constructor(context: vscode.ExtensionContext, iacPath: vscode.Uri) {
21 | // Create a new sqlite3 database in the global storage directory
22 | this.pdb = null;
23 | this.iacPath = iacPath;
24 | this.context = context;
25 | }
26 |
27 | async init(): Promise {
28 | assert(this.pdb === null, "ProjectDB init called twice");
29 | console.log("[IaC ProjectDir] Init");
30 | if (this.pdb !== null) { return; }
31 | this.pdb = await this.initProjectDb(this.iacPath);
32 | return;
33 | }
34 |
35 | async initProjectDb(iacPath: vscode.Uri) {
36 | let relDbPath = constants.PROJECT_DIR_DB;
37 | let dbPath = iacPath.fsPath + '/' + relDbPath;
38 | let db = await open({
39 | filename: dbPath,
40 | driver: sqlite3.Database
41 | });
42 | await this.createDbTables(db);
43 | return db;
44 | }
45 |
46 | async createDbTables(db: any) {
47 | await db.exec(`CREATE TABLE IF NOT EXISTS projects (
48 | uuid TEXT NOT NULL PRIMARY KEY,
49 | name TEXT NOT NULL,
50 | keys TEXT NULL,
51 | jwt TEXT NULL DEFAULT NULL,
52 | deleted INTEGER DEFAULT 0
53 | )`);
54 | }
55 |
56 | async listProjects(): Promise {
57 | if (this.pdb === null) {
58 | return [];
59 | }
60 | let stmt = await this.pdb.prepare('SELECT uuid, name, keys FROM projects WHERE deleted = 0');
61 | let rows = await stmt.all({});
62 | await stmt.finalize();
63 | return rows as any[];
64 | }
65 |
66 | async pushProject(uuid: string, name: string, keys: string | null, jwt: string | null = null) {
67 | if (this.pdb === null) {
68 | return;
69 | }
70 | if (jwt === null && keys !== null) {
71 | let mIaCEncryption = new IaCEncryption();
72 | await mIaCEncryption.setKey(keys);
73 | jwt = await mIaCEncryption.encrypt({ uuid: uuid });
74 | }
75 |
76 | let stmt = await this.pdb.prepare('INSERT OR REPLACE INTO projects (uuid, name, keys, jwt) VALUES (?, ?, ?, ?)');
77 | console.log(`[IaC Projects] pushProject(${uuid}, ${name}, ${keys})`);
78 | assert(uuid !== null);
79 | await stmt.run(uuid, name, keys, jwt);
80 | await stmt.finalize();
81 | }
82 |
83 | async removeProject(uuid: string, rdb: RemoteDB | null = null) {
84 | if (this.pdb === null) {
85 | return;
86 | }
87 | let stmt = await this.pdb.prepare('UPDATE projects SET deleted = 1, keys = NULL, jwt = NULL, name = \'\' WHERE uuid = ?');
88 | await stmt.run(uuid);
89 | await stmt.finalize();
90 |
91 | if (rdb !== null) {
92 | await this.syncProjects(rdb);
93 | }
94 | }
95 |
96 | async getProject(uuid: string): Promise<[string, string, string, string] | null> {
97 | if (this.pdb === null) {
98 | return null;
99 | }
100 | console.log(`[IaC Projects] getProject(${uuid})`);
101 | console.log(`[IaC Projects] list of projects:`);
102 | (await this.listProjects()).forEach((project: any) => {
103 | console.log(`[IaC Projects] ${project.uuid} ${project.name} ${project.keys}`);
104 | });
105 |
106 |
107 | let stmt = await this.pdb.prepare('SELECT uuid, name, keys, jwt FROM projects WHERE uuid = ? AND deleted = 0');
108 | let row: any = await stmt.get(uuid);
109 | await stmt.finalize();
110 | if (row === undefined || row === null) {
111 | return null;
112 | }
113 | return [row.uuid, row.name, row.keys, row.jwt];
114 | }
115 |
116 | async close() {
117 | if (this.pdb === null) {
118 | return;
119 | }
120 | await this.pdb.close();
121 | }
122 |
123 | async safeSyncProjects(rdb: RemoteDB) {
124 | if (this.currentSync !== undefined) {
125 | console.log("[IaC Projects] Sync already in progress, skipping");
126 | return;
127 | }
128 | this.currentSync = this.syncProjects(rdb).finally(() => {
129 | this.currentSync = undefined;
130 | });
131 | await this.currentSync;
132 | }
133 |
134 | async syncProjects(rdb: RemoteDB): Promise {
135 | console.log(`[IaC Projects] syncProjects()`);
136 | if (!rdb.settingsEnabledAndConfigured()) { return; }
137 | if (this.pdb === null) { return; }
138 | let res = await rdb.listProjects();
139 | if (res === null) { return; }
140 | let [remoteProjects, remoteDeletedProjects] = res;
141 | remoteDeletedProjects = remoteDeletedProjects.map((row: any) => row.uuid);
142 |
143 | let remoteProjectUuids = remoteProjects.map((row: any) => row.uuid);
144 | console.log(`[IaC Projects] remoteProjects: ${remoteProjectUuids} total ${remoteProjects.length}`);
145 |
146 | let stmt = await this.pdb.prepare('SELECT uuid, name, jwt, deleted FROM projects');
147 | let rows = await stmt.all();
148 | await stmt.finalize();
149 | let localProjects = rows.filter((row: any) => row.deleted !== 1);
150 | let localDeletedProjects = rows.filter((row: any) => row.deleted === 1).map((row: any) => row.uuid);
151 |
152 | // Delete local projects that have been deleted remotely
153 | console.log(`[IaC Projects] remoteDeletedProjects: ${remoteDeletedProjects}`);
154 | for (let uuid of remoteDeletedProjects) {
155 | console.log(`[IaC Projects] uuid: ${uuid}`);
156 | if (localProjects.some((row: any) => row.uuid === uuid)) {
157 | await this.removeProject(uuid);
158 | // Remove the project from the local list so it doesn't get added again
159 | localProjects = localProjects.filter((row: any) => row.uuid !== uuid);
160 | // Remove local project database
161 | // TODO: remove duplicate code from main file
162 | let dbDir = path.join(this.context.globalStorageUri.fsPath, constants.IAC_FOLDER_NAME);
163 | let dbFilename = path.basename(`/${constants.EXT_NAME}-${uuid}.db`);
164 | let dbPath = path.join(dbDir, dbFilename);
165 | let db = new LocalDB(dbPath);
166 | await db.init();
167 | await db.dropAllTables();
168 | await db.close();
169 | } else if (rows.filter((row: any) => row.uuid === uuid).length === 0) {
170 | // If the project doesn't exist locally, add it as deleted
171 | console.log(`[IaC Projects] Adding remote project as deleted: ${uuid}`);
172 | await this.pushProject(uuid, "", null);
173 | await this.removeProject(uuid);
174 | }
175 | }
176 |
177 | // Delete remote projects that have been deleted locally
178 | for (let uuid of localDeletedProjects) {
179 | if (remoteProjects.some((proj: any) => proj.uuid === uuid) || (!remoteDeletedProjects.some((uuid2: any) => uuid2 === uuid))) {
180 | console.log(`[IaC Projects] Removing remote project ${uuid} as was deleted locally`);
181 | await rdb.pushProject(uuid, "", null, true);
182 |
183 | // Remove the project from the remote list so it doesn't get added again
184 | remoteProjects = remoteProjects.filter((proj: any) => proj.uuid !== uuid);
185 | }
186 | }
187 |
188 | // Push local projects that don't exist remotely
189 | for (let row of localProjects) {
190 | if (!remoteProjects.some((proj: any) => proj.uuid === row.uuid)) {
191 | console.log(`[IaC Projects] Pushing local project: ${row.uuid}`);
192 | await rdb.pushProject(row.uuid, row.name, row.jwt, false);
193 | }
194 | }
195 |
196 | // Pull remote projects that don't exist locally
197 | for (let proj of remoteProjects) {
198 | let uuid = proj.uuid;
199 | if (localProjects.some((row: any) => row.uuid === uuid)) {
200 | console.log(`[IaC Projects] Project already exists locally: ${uuid}`);
201 | continue;
202 | }
203 | console.log(`[IaC Projects] Pulling remote project into local db: ${uuid}`);
204 | await this.pushProject(proj.uuid, proj.name, null, proj.jwt);
205 | }
206 |
207 | console.log(`[IaC Projects] syncProjects() done`);
208 |
209 | return;
210 | }
211 | }
--------------------------------------------------------------------------------
/src/semgrep.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync } from "fs";
2 | import * as path from 'path';
3 | import { EXT_COMMON_NAME, SEMGREP_TIMEOUT_MS } from './constants';
4 | import * as which from "which";
5 | import * as child_process from "child_process";
6 | import * as vscode from "vscode";
7 | import * as constants from "./constants";
8 | import { IaCDiagnostics } from "./diagnostics";
9 | import * as util from "util";
10 | import { assert } from "console";
11 |
12 | export function getSemgrepPath(): string | undefined {
13 | let res = which.sync("semgrep", { nothrow: true });
14 | if ((res === undefined) || (res === null)) {
15 | const resp = vscode.window.showInformationMessage(
16 | `Semgrep not installed. Please install Semgrep to use ${EXT_COMMON_NAME}.`,
17 | );
18 | return undefined;
19 | } else {
20 | return res;
21 | }
22 | }
23 |
24 | let hideProgressBar: any = undefined;
25 | let gmIaCDiagnostics: IaCDiagnostics;
26 |
27 | export async function runSemgrep(context: vscode.ExtensionContext, path: string, mIaCDiagnostics: IaCDiagnostics): Promise {
28 | let semgrepPath = getSemgrepPath();
29 | if (semgrepPath === undefined) { return; }
30 | if (hideProgressBar) {
31 | vscode.window.showErrorMessage("Semgrep is already running.");
32 | return;
33 | }
34 |
35 | vscode.window.withProgress({
36 | location: { viewId: 'iacAudit' }
37 | }, (progress, token) => {
38 | return new Promise((resolve, reject) => {
39 | hideProgressBar = resolve;
40 | });
41 | });
42 |
43 | gmIaCDiagnostics = mIaCDiagnostics;
44 | // Load semgrepArgs from settings
45 | let semgrepArgs: string | undefined = vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).get('semgrepArgs');
46 | if ((semgrepArgs === undefined) || (semgrepArgs.trim() === "")) {
47 | semgrepArgs = "--config auto";
48 | }
49 | let semgrepArgsArray = semgrepArgs.split(" ");
50 | if (vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).get('enableIaC')) {
51 | console.log(`[IaC Semgrep] Running Semgrep with IaC rules`);
52 | semgrepArgsArray = semgrepArgsArray.concat(["--config", context.asAbsolutePath("rules/")]);
53 | }
54 | else {
55 | console.log(`[IaC Semgrep] Running Semgrep WITHOUT IaC rules`);
56 | }
57 | semgrepArgsArray = ["--json", "--quiet"].concat(semgrepArgsArray).concat(["./"]);
58 | console.log(`[IaC Semgrep] Running Semgrep with args: ${semgrepArgsArray}`);
59 |
60 | // Load semgrepTimeout from settings
61 | let semgrepTimeout: number | undefined = vscode.workspace.getConfiguration(`${constants.EXT_NAME}`).get('semgrepTimeout');
62 | if ((semgrepTimeout === undefined) || (semgrepTimeout < 0)) {
63 | assert(false, `Invalid semgrepTimeout value: ${semgrepTimeout}`);
64 | console.error(`[IaC Semgrep] Invalid semgrepTimeout value: ${semgrepTimeout}`);
65 | return;
66 | }
67 |
68 | let execFile = util.promisify(child_process.execFile);
69 | try {
70 | let {stderr, stdout} = await execFile(semgrepPath, semgrepArgsArray, { timeout: semgrepTimeout * 1000, cwd: path, maxBuffer: constants.SEMGREP_MAX_BUFFER });
71 | if (stderr) {
72 | console.log(`[IaC Semgrep] stderr: ${stderr}`);
73 | vscode.window.showErrorMessage(`Semgrep error: ${stderr}`);
74 | return;
75 | }
76 | console.log(`[IaC Semgrep] Semgrep done, stdout: ${stdout}`);
77 |
78 | let jsonParsed = { parsed: JSON.parse(stdout), source: "semgrep-local" };
79 | await gmIaCDiagnostics.clearDiagnostics();
80 | await gmIaCDiagnostics.loadDiagnosticsFromSemgrep(jsonParsed);
81 | }
82 | catch (error: any) {
83 | console.log(`[IaC Semgrep] error: ${error.message}`);
84 | let timeoutFormatted = semgrepTimeout.toFixed(2);
85 | let msg = `Semgrep timeout (${timeoutFormatted}s) exceeded or execution error. Error: ${error.message}`;
86 | vscode.window.showErrorMessage(msg);
87 | }
88 | finally {
89 | if (hideProgressBar) {
90 | hideProgressBar();
91 | hideProgressBar = undefined;
92 | }
93 | }
94 | }
95 |
96 | export async function runSemgrepHcl(context: vscode.ExtensionContext, wspath: string, iacpath: string): Promise {
97 | let semgrepPath = getSemgrepPath();
98 | if (semgrepPath === undefined) { return null; }
99 |
100 | console.log(`[IaC Semgrep] Semgrep iacpath: ${iacpath}`);
101 | console.log(`[IaC Semgrep] Semgrep wspath: ${wspath}`);
102 | iacpath = path.relative(wspath, iacpath);
103 | console.log(`[IaC Semgrep] Semgrep CWD: ${iacpath}`);
104 |
105 | if (iacpath.trim() === "") {
106 | iacpath = "./";
107 | }
108 |
109 | let semgrepArgsArray: string[] = ["--config", context.asAbsolutePath("tfparse_rules/")];
110 | semgrepArgsArray = ["--no-git-ignore", "--json", "--quiet"].concat(semgrepArgsArray).concat([iacpath]);
111 | console.log(`[IaC Semgrep] Running Semgrep with args: ${semgrepArgsArray}`);
112 |
113 | let execFile = util.promisify(child_process.execFile);
114 |
115 | try {
116 | let {stderr, stdout} = await execFile(semgrepPath, semgrepArgsArray, { timeout: SEMGREP_TIMEOUT_MS, cwd: wspath, maxBuffer: constants.SEMGREP_MAX_BUFFER });
117 | if (stderr) {
118 | console.log(`[IaC Semgrep] stderr: ${stderr}`);
119 | vscode.window.showErrorMessage(`Semgrep error: ${stderr}`);
120 | return null;
121 | }
122 | console.log(`[IaC Semgrep] Semgrep done, stdout: ${stdout}`);
123 | return stdout;
124 | }
125 | catch (error: any) {
126 | console.log(`[IaC Semgrep] error: ${error.message}`);
127 | let timeoutFormatted = (SEMGREP_TIMEOUT_MS / 1000).toFixed(2);
128 | let msg = `Semgrep timeout (${timeoutFormatted}s) exceeded or execution error. Error: ${error.message}`;
129 | vscode.window.showErrorMessage(msg);
130 | return null;
131 | }
132 | }
133 |
134 | export function parseSemgrep(jsonPath: any): any {
135 | let jsonString = readFileSync(jsonPath, { encoding: "utf8" });
136 | let jsonParsed = JSON.parse(jsonString);
137 | return { parsed: jsonParsed, path: path.dirname(jsonPath) + '/', source: "semgrep-imported" };
138 | }
--------------------------------------------------------------------------------
/src/test/runTest.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | import { runTests } from '@vscode/test-electron';
4 |
5 | async function main() {
6 | try {
7 | // The folder containing the Extension Manifest package.json
8 | // Passed to `--extensionDevelopmentPath`
9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../');
10 |
11 | // The path to test runner
12 | // Passed to --extensionTestsPath
13 | const extensionTestsPath = path.resolve(__dirname, './suite/index');
14 |
15 | // Download VS Code, unzip it and run the integration test
16 | await runTests({ extensionDevelopmentPath, extensionTestsPath });
17 | } catch (err) {
18 | console.error('Failed to run tests', err);
19 | process.exit(1);
20 | }
21 | }
22 |
23 | main();
24 |
--------------------------------------------------------------------------------
/src/test/suite/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert';
2 |
3 | // You can import and use all API from the 'vscode' module
4 | // as well as import your extension to test it
5 | import * as vscode from 'vscode';
6 | // import * as myExtension from '../../extension';
7 |
8 | suite('Extension Test Suite', () => {
9 | vscode.window.showInformationMessage('Start all tests.');
10 |
11 | test('Sample test', () => {
12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5));
13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0));
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/test/suite/index.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as Mocha from 'mocha';
3 | import * as glob from 'glob';
4 |
5 | export function run(): Promise {
6 | // Create the mocha test
7 | const mocha = new Mocha({
8 | ui: 'tdd',
9 | color: true
10 | });
11 |
12 | const testsRoot = path.resolve(__dirname, '..');
13 |
14 | return new Promise((c, e) => {
15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
16 | if (err) {
17 | return e(err);
18 | }
19 |
20 | // Add files to the test suite
21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
22 |
23 | try {
24 | // Run the mocha test
25 | mocha.run(failures => {
26 | if (failures > 0) {
27 | e(new Error(`${failures} tests failed.`));
28 | } else {
29 | c();
30 | }
31 | });
32 | } catch (err) {
33 | console.error(err);
34 | e(err);
35 | }
36 | });
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/tree.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as fs from 'fs';
3 | import * as path from 'path';
4 | import * as util from 'util';
5 |
6 | import * as projects from './projects';
7 | import * as constants from './constants';
8 | import * as remote from './remote';
9 |
10 | export class ProjectTreeViewManager {
11 | private context: vscode.ExtensionContext;
12 | private pdb: projects.IaCProjectDir;
13 | private rdb: remote.RemoteDB | undefined;
14 | private projectTreeView: vscode.TreeView | undefined = undefined;
15 | private projectTreeDataProvider: ProjectSelectorProvider | undefined = undefined;
16 | private originalViewTitle: string | undefined = undefined;
17 | private _disposables: vscode.Disposable[] = [];
18 | private hideProgressBar: any = undefined;
19 |
20 | constructor(context: vscode.ExtensionContext, pbd: projects.IaCProjectDir, rdb: remote.RemoteDB | undefined) {
21 | this.context = context;
22 | this.pdb = pbd;
23 | this.rdb = rdb;
24 |
25 | this._disposables.push(vscode.commands.registerCommand('iacAudit.refreshProjectTree', async () => {
26 | if (this.hideProgressBar !== undefined) {
27 | console.log("[IaC Tree] Already refreshing project list");
28 | return;
29 | }
30 |
31 | vscode.window.withProgress({
32 | location: { viewId: 'iacAudit' }
33 | }, (progress, token) => {
34 | return new Promise((resolve, reject) => {
35 | this.hideProgressBar = resolve;
36 | });
37 | });
38 |
39 | try {
40 | await this.update();
41 | }
42 | catch (err: any) {
43 | console.error(`[IaC Tree] Error refreshing project list: ${err}`);
44 | }
45 | finally {
46 | if (this.hideProgressBar !== undefined) {
47 | this.hideProgressBar();
48 | this.hideProgressBar = undefined;
49 | }
50 | }
51 | }));
52 |
53 | this._disposables.push(vscode.commands.registerCommand('iacAudit.deleteTreeProject', async (project: ProjectItem) => {
54 | console.log(`[IaC Tree] Deleting project ${project.uuid}`);
55 | vscode.window.showWarningMessage('Are you sure you want to destroy the selected project (remove also on remote)?', 'Yes', 'No').then(async (choice) => {
56 | if (choice === 'Yes') {
57 | await this.pdb.removeProject(project.uuid, rdb);
58 | await this.update();
59 | }
60 | });
61 | }));
62 | }
63 |
64 | async show() {
65 | this.projectTreeDataProvider = new ProjectSelectorProvider([]);
66 | this.projectTreeView = vscode.window.createTreeView('iacAudit', {
67 | treeDataProvider: this.projectTreeDataProvider,
68 | });
69 | this.originalViewTitle = this.projectTreeView.title;
70 | this.projectTreeView.title = constants.PROJECT_TREE_VIEW_TITLE;
71 | this.projectTreeView.message = undefined;
72 | this.projectTreeView.onDidChangeSelection(async (e) => {
73 | if (e.selection.length !== 1) {
74 | console.log(`[IaC Tree] Invalid selection length: ${e.selection}`);
75 | return;
76 | }
77 | let project = e.selection[0];
78 | if (project instanceof ProjectItem) {
79 | console.log(`[IaC Tree] Opening project ${project.uuid}`);
80 | await vscode.commands.executeCommand(`${constants.EXT_NAME}.openProject`, project.uuid);
81 | return;
82 | }
83 | console.log(`[IaC Tree] Invalid selection: ${e.selection}`);
84 | });
85 | }
86 |
87 | async hide() {
88 | if (this.projectTreeView === undefined) {
89 | return;
90 | }
91 | this.projectTreeView.title = this.originalViewTitle || "";
92 | this.projectTreeView.message = undefined;
93 | await this.projectTreeView?.dispose();
94 | }
95 |
96 | async showDbError() {
97 | if (this.projectTreeView === undefined) {
98 | await this.show();
99 | }
100 | (this.projectTreeView as vscode.TreeView).message = constants.PROJECT_TREE_VIEW_DB_ERROR_MESSAGE;
101 | }
102 |
103 | private async syncRemoteDB(): Promise {
104 | if (this.rdb === undefined) {
105 | return false;
106 | }
107 |
108 | if (this.rdb.settingsEnabled()) {
109 | // Promisify onDbReadyOnce
110 | let onDbReadyOnce = (): Promise => {
111 | let rrdb = this.rdb;
112 | return new Promise(function(resolve, reject) {
113 | if (rrdb === undefined) {
114 | resolve();
115 | }
116 | else {
117 | rrdb.onDbReadyOnce(resolve);
118 | }
119 | });
120 | }
121 |
122 | try {
123 | await onDbReadyOnce();
124 | await this.pdb.safeSyncProjects(this.rdb);
125 | }
126 | catch (err) {
127 | console.error(`[IaC Tree] Error syncing remote DB: ${err}`);
128 | return false;
129 | }
130 | return true;
131 | }
132 | else {
133 | return false;
134 | }
135 | }
136 |
137 | async update(projectList: {}[] | null = null) {
138 | if (projectList == null) {
139 | await this.syncRemoteDB();
140 | projectList = (await this.pdb.listProjects()) as {}[];
141 | }
142 | if (this.projectTreeView !== undefined) {
143 | this.projectTreeDataProvider?.update(projectList);
144 | if (projectList.length === 0) {
145 | this.projectTreeView.message = constants.PROJECT_TREE_VIEW_NO_PROJECTS_MESSAGE;
146 | }
147 | }
148 | }
149 |
150 | async dispose() {
151 | await this.hide();
152 |
153 | this._disposables.forEach((disposable) => {
154 | disposable.dispose();
155 | });
156 | }
157 | }
158 |
159 | class ProjectSelectorProvider implements vscode.TreeDataProvider {
160 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
161 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
162 |
163 | constructor(private projectList: any[]) { }
164 |
165 | update(projectList: any[]) {
166 | this.projectList = projectList;
167 | this._onDidChangeTreeData.fire();
168 | }
169 |
170 | getTreeItem(element: ProjectItem): vscode.TreeItem {
171 | return element;
172 | }
173 |
174 | getChildren(element?: ProjectItem): Thenable {
175 | if (element) {
176 | return Promise.resolve([]);
177 | } else {
178 | return Promise.resolve(this.getProjects());
179 | }
180 | }
181 |
182 | private getProjects(): ProjectItem[] {
183 | return this.projectList.map(
184 | (project: any) =>
185 | new ProjectItem(project.uuid, project.name, vscode.TreeItemCollapsibleState.None)
186 | );
187 | }
188 | }
189 |
190 | class ProjectItem extends vscode.TreeItem {
191 | constructor(
192 | public uuid: string,
193 | private pname: string,
194 | public readonly collapsibleState: vscode.TreeItemCollapsibleState
195 | ) {
196 | const label = pname;
197 | super(label, collapsibleState);
198 | this.tooltip = `${this.label} $ ${this.uuid}`;
199 | this.description = this.uuid;
200 | }
201 |
202 | // TODO: add icons for projects
203 | //iconPath = {
204 | // light: path.join(__filename, '..', '..', 'resources', 'light', 'dependency.svg'),
205 | // dark: path.join(__filename, '..', '..', 'resources', 'dark', 'dependency.svg')
206 | //};
207 | }
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import * as path from 'path';
3 | import { parseSemgrep, runSemgrep, runSemgrepHcl } from './semgrep';
4 | import { IaCDiagnostics } from './diagnostics';
5 | import { RemoteDB } from './remote';
6 | import { assert } from 'console';
7 |
8 | export async function handleOpenSemgrepJson(context: vscode.ExtensionContext, mIaCDiagnostics: IaCDiagnostics) {
9 | let jsonFile = await chooseJsonFile();
10 | console.log("[Semgrep] " + jsonFile?.fsPath);
11 | if (!jsonFile) { return; }
12 | console.log("[Semgrep] OK1 ");
13 | let jsonParsed = parseSemgrep(jsonFile?.fsPath);
14 | console.log("[Semgrep] OK2 ");
15 |
16 | await mIaCDiagnostics.loadDiagnosticsFromSemgrep(jsonParsed);
17 | console.log("[Semgrep] OK3 ");
18 | }
19 |
20 | export async function handleStartSemgrepScan(context: vscode.ExtensionContext, mIaCDiagnostics: IaCDiagnostics | null = null, iacPath: string | null = null): Promise {
21 | console.log("Starting semgrep scan");
22 | // Run semgrep on current workspace
23 | // TODO: run on all workspace folders, not just first one
24 | if (vscode.workspace.workspaceFolders === undefined) {
25 | vscode.window.showErrorMessage("Cannot run Semgrep. No workspace folder is open.");
26 | return null;
27 | }
28 | let wspath = vscode.workspace.workspaceFolders[0].uri.fsPath;
29 | if ((iacPath == null) && (mIaCDiagnostics != null)) {
30 | await runSemgrep(context, wspath, mIaCDiagnostics);
31 | return null;
32 | }
33 | else if (iacPath != null) {
34 | return await runSemgrepHcl(context, wspath, iacPath);
35 | }
36 | else {
37 | assert(false, "[Semgrep:handleStartSemgrepScan]: either mIaCDiagnostics or iacPath must be provided");
38 | return null;
39 | }
40 | }
41 |
42 | async function chooseJsonFile(): Promise {
43 | return vscode.window.showOpenDialog({
44 | openLabel: "Choose Semgrep result",
45 | canSelectFiles: true,
46 | canSelectFolders: false,
47 | canSelectMany: false,
48 | filters: { JsonFiles: ['json', 'txt'] }
49 | })
50 | .then((chosen) => chosen && chosen?.length > 0 ? chosen[0] : undefined);
51 | }
52 |
53 | export function genUUID(): string {
54 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
55 | let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
56 | return v.toString(16);
57 | });
58 | }
59 |
60 | export function relPathToAbs(relPath: string) {
61 | if (vscode.workspace.workspaceFolders === undefined) {
62 | return relPath;
63 | }
64 |
65 | const absPath = path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, relPath);
66 | return absPath;
67 | }
68 |
69 | export function absPathToRel(absPath: string) {
70 | if (vscode.workspace.workspaceFolders === undefined) {
71 | return absPath;
72 | }
73 |
74 | const relPath = path.relative(vscode.workspace.workspaceFolders[0].uri.fsPath, absPath);
75 | return relPath;
76 | }
77 |
78 | export async function ensureDirExists(dirPath: string): Promise {
79 | let dirUri = vscode.Uri.file(dirPath);
80 |
81 | return await vscode.workspace.fs.stat(dirUri).then(
82 | async () => {
83 | console.log('[IaC Utils] Storage directory exists');
84 | return true;
85 | },
86 | async () => {
87 | console.log('[IaC Utils] Storage directory does not exist');
88 | await vscode.workspace.fs.createDirectory(dirUri);
89 |
90 | return await vscode.workspace.fs.stat(dirUri).then(
91 | () => {
92 | console.log('[IaC Main] Storage directory created');
93 | return true;
94 | },
95 | () => {
96 | console.log('[IaC Main] Storage directory could not be created');
97 |
98 | // Show an error message
99 | vscode.window.showErrorMessage('Storage directory could not be created');
100 |
101 | return false;
102 | }
103 | );
104 | }
105 | );
106 | }
--------------------------------------------------------------------------------
/tfparse_rules/hcl-resource.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | - id: hcl-resource
3 | patterns:
4 | - pattern: |
5 | resource $RT $RN {
6 | ...
7 | }
8 | message: "IaC resource block: $RT $RN"
9 | languages:
10 | - hcl
11 | severity: WARNING
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "ES2020",
5 | "outDir": "out",
6 | "lib": [
7 | "ES2020"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true, /* enable all strict type-checking options */
12 | /* Additional Checks */
13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
16 | }
17 | }
18 |
--------------------------------------------------------------------------------