-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(threat-modeler): add genAI threat modeling action
- Loading branch information
1 parent
2f02738
commit 10db8fa
Showing
10 changed files
with
2,629 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Note: This action creates a baseline threat model for you to get started with. You're strongly encouraged to expand upon the results. Please review all results before approving any automated PRs | ||
name: GenAI Threat Modeling Assistant | ||
description: Create a threat model based on an application description and mermaid.js diagram using the ChatGPT API | ||
author: 'Kong' | ||
inputs: | ||
docs-directory: | ||
description: 'The directory where your markdown docs are located' | ||
required: true | ||
default: '' | ||
application-description: | ||
description: '2-3 sentences describing the target application' | ||
required: true | ||
default: '' | ||
# Example: MY_APP is a Java application, deployed as a microservice, running as a Docker container on AWS EKS. MY_APP exposes an API, which provides consumers the ability to submit JWT tokens, whose claims are evaluated against a rules engine. MY_APP returns a true/false response, which consumers can use to determine whether the request is authorized or not. | ||
dry-run: | ||
description: "Process all the files, build the ChatGPT params, but don't call the ChatGPT API" | ||
required: false | ||
default: 'false' | ||
api-key: | ||
description: "OPENAI API key" | ||
required: true | ||
default: '' | ||
|
||
runs: | ||
using: 'composite' | ||
steps: | ||
- name: Checkout Git repository | ||
uses: actions/checkout@v3 | ||
with: | ||
repository: Sanboxy/public-shared-actions | ||
path: './threat-modeler' | ||
|
||
- name: Call Local Action | ||
uses: ./threat-modeler/threat-modeler | ||
id: threat-modeler | ||
with: | ||
description: ${{ inputs.application-description }} | ||
directory: ../../${{ inputs.docs-directory }} | ||
dry-run: ${{ inputs.dry-run }} | ||
api-key: ${{ inputs.api-key }} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
name: Threat Model | ||
|
||
on: | ||
pull_request: {} | ||
push: | ||
branches: | ||
- master | ||
- main | ||
workflow_dispatch: {} | ||
|
||
jobs: | ||
threat-modeler: | ||
name: ChatGPT Threat Model | ||
runs-on: ubuntu-latest | ||
|
||
if: (github.actor != 'dependabot[bot]') | ||
|
||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v3 | ||
|
||
- name: Run Threat-Modeler | ||
id: threat-modeler | ||
uses: Kong/public-shared-actions@91c2c6a12cc7baf69aea166f3ca9b3528ed018c9 | ||
with: | ||
docs-directory: 'docs' | ||
application-description: 'application_description' | ||
dry-run: 'false' | ||
api-key: ${{ secrets.OPENAI_API_KEY}} | ||
|
||
- name: Download artifacts | ||
uses: actions/download-artifact@v4 | ||
with: | ||
name: threat-model | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
name: run node | ||
inputs: | ||
directory: | ||
description: 'The directory where your markdown docs are located' | ||
required: true | ||
default: '' | ||
description: | ||
description: '2-3 sentences describing the target application' | ||
required: true | ||
default: '' | ||
dry-run: | ||
description: "Process all the files, build the ChatGPT params, but don't call the ChatGPT API" | ||
required: false | ||
default: 'false' | ||
api-key: | ||
description: "OPENAI API key" | ||
required: true | ||
default: '' | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- name: Set up Node.js | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: '20.x' | ||
|
||
- name: Install dependencies | ||
working-directory: ./threat-modeler/threat-modeler | ||
shell: bash | ||
run: npm i | ||
|
||
- name: Build TS | ||
working-directory: ./threat-modeler/threat-modeler | ||
shell: bash | ||
run: npm run build | ||
|
||
- name: Run threat-modeler | ||
id: threat-modeler | ||
working-directory: ./threat-modeler/threat-modeler | ||
shell: bash | ||
env: | ||
OPENAI_API_KEY: ${{ inputs.api-key }} | ||
run: npm run start -- '${{ inputs.description }}' '${{ inputs.directory }}' '${{ inputs.dry-run }}' > threat_model.json | ||
|
||
- name: Archive threat modeler results | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: threat-model | ||
path: threat-modeler/threat-modeler/threat_model.json | ||
|
148 changes: 148 additions & 0 deletions
148
security-actions/threat-modeler/threat-modeler/dist/main.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const openai_1 = require("openai"); | ||
const fs = require("fs"); | ||
const glob_1 = require("glob"); | ||
var STRIDE; | ||
(function (STRIDE) { | ||
STRIDE["S"] = "SPOOFING"; | ||
STRIDE["T"] = "TAMPERING"; | ||
STRIDE["R"] = "REPUDIATION"; | ||
STRIDE["I"] = "INFORMATION DISCLOSURE"; | ||
STRIDE["D"] = "DENIAL OF SERVICE"; | ||
STRIDE["E"] = "ELEVATION OF PRIVILEGE"; | ||
})(STRIDE || (STRIDE = {})); | ||
var LIKELIHOOD; | ||
(function (LIKELIHOOD) { | ||
LIKELIHOOD["RARE"] = "RARE"; | ||
LIKELIHOOD["UNLIKELY"] = "UNLIKELY"; | ||
LIKELIHOOD["PROBABLE"] = "PROBABLE"; | ||
LIKELIHOOD["LIKELY"] = "LIKELY"; | ||
LIKELIHOOD["ALMOST_CERTAIN"] = "ALMOST CERTAIN"; | ||
})(LIKELIHOOD || (LIKELIHOOD = {})); | ||
var IMPACT; | ||
(function (IMPACT) { | ||
IMPACT[IMPACT["INSIGNIFICANT"] = 0] = "INSIGNIFICANT"; | ||
IMPACT[IMPACT["MINIMAL"] = 1] = "MINIMAL"; | ||
IMPACT[IMPACT["MODERATE"] = 2] = "MODERATE"; | ||
IMPACT[IMPACT["SIGNIFICANT"] = 3] = "SIGNIFICANT"; | ||
IMPACT[IMPACT["CATASTROPHIC"] = 4] = "CATASTROPHIC"; | ||
})(IMPACT || (IMPACT = {})); | ||
var RISK; | ||
(function (RISK) { | ||
RISK[RISK["MINOR"] = 0] = "MINOR"; | ||
RISK[RISK["LOW"] = 1] = "LOW"; | ||
RISK[RISK["MEDIUM"] = 2] = "MEDIUM"; | ||
RISK[RISK["HIGH"] = 3] = "HIGH"; | ||
RISK[RISK["EXTREME"] = 4] = "EXTREME"; | ||
})(RISK || (RISK = {})); | ||
const main = () => __awaiter(void 0, void 0, void 0, function* () { | ||
const args = process.argv.slice(2); | ||
const dryRun = getDryRun(args[2]); | ||
const systemPrompt = fs.readFileSync('prompt.txt', 'utf-8'); | ||
const mermaidMatrix = yield getMermaidMatrix(args[1]); | ||
let rootModel = []; | ||
for (const mermaids of mermaidMatrix) { | ||
for (const mermaid of mermaids) { | ||
const res = yield request(dryRun, systemPrompt, args[0], mermaid); | ||
rootModel = appendNewModel(res, rootModel); | ||
} | ||
} | ||
if (!dryRun) { | ||
console.log(rootModel); | ||
} | ||
}); | ||
const getMermaidMatrix = (directory) => __awaiter(void 0, void 0, void 0, function* () { | ||
// Strip slash suffix if present | ||
let dir = directory; | ||
if (directory.endsWith('/')) { | ||
dir = directory.slice(0, -1); | ||
} | ||
const files = yield (0, glob_1.glob)(dir + '/**/*.md'); | ||
const mermaidMatrix = []; | ||
// Get all mermaid blocks | ||
files.forEach((file) => { | ||
mermaidMatrix.push(extractMermaid(fs.readFileSync(file, 'utf-8').split(/\r?\n/))); | ||
}); | ||
return mermaidMatrix; | ||
}); | ||
const extractMermaid = (contents) => { | ||
const mermaids = []; | ||
let mermaid = ''; | ||
let inMermaidBlock = false; | ||
contents.forEach((line) => { | ||
if (inMermaidBlock) { | ||
mermaid += '\r\n' + line; | ||
} | ||
if (line === '```mermaid') { | ||
inMermaidBlock = true; | ||
mermaid += line; | ||
} | ||
if (line === '```') { | ||
mermaids.push(mermaid); | ||
mermaid = ''; | ||
inMermaidBlock = false; | ||
} | ||
}); | ||
return mermaids; | ||
}; | ||
const request = (dryRun, systemPrompt, description, mermaid) => __awaiter(void 0, void 0, void 0, function* () { | ||
const params = { | ||
response_format: { type: 'json_object' }, | ||
messages: [ | ||
{ | ||
role: 'user', | ||
content: `${description} | ||
Mermaid: | ||
${mermaid} | ||
Create the threat model and only return a JSON response, using the schema provided.`, | ||
}, | ||
{ | ||
role: 'system', | ||
content: systemPrompt, | ||
}, | ||
], | ||
model: 'gpt-3.5-turbo', | ||
}; | ||
if (dryRun === false) { | ||
const openai = new openai_1.default({ | ||
apiKey: process.env.OPENAI_SECRET_KEY, | ||
}); | ||
const chatCompletion = yield openai.chat.completions.create(params); | ||
const content = chatCompletion.choices[0].message.content; | ||
return JSON.parse(content).threats; | ||
} | ||
console.log('GPT params:', params); | ||
return []; | ||
}); | ||
const getDryRun = (flag) => { | ||
if (flag === 'true') { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
const appendNewModel = (model, rootModel) => { | ||
if (rootModel.length !== 0) { | ||
const lastId = rootModel[rootModel.length - 1].id; | ||
model.forEach((threat, index) => { | ||
index++; | ||
threat.id = lastId + index; | ||
rootModel.push(threat); | ||
}); | ||
return rootModel; | ||
} | ||
rootModel.push(...model); | ||
return rootModel; | ||
}; | ||
main(); |
Oops, something went wrong.