Repository navigation
New Components - Darkmoon #22135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
New Components - Darkmoon #22135
Changes from all commits
5382d51
9f099be
8b5caed
f8f07bb
6569d70
d3a8b68
e2e61a1
eae3ad5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-get-campaign-report", | ||
| name: "Get Campaign Report", | ||
| description: "Retrieve the generated report of a Darkmoon campaign. The `content` field holds the report body (markdown by default). A report that is not generated yet comes back as a placeholder starting with `# Report not found`. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: false, | ||
| readOnlyHint: true, | ||
| }, | ||
| props: { | ||
| app, | ||
| campaignId: { | ||
| propDefinition: [ | ||
| app, | ||
| "campaignId", | ||
| ], | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.getCampaignReport({ | ||
| $, | ||
| campaignId: this.campaignId, | ||
| }); | ||
| const ready = Boolean(response?.content) | ||
| && !response.content.startsWith("# Report not found"); | ||
| $.export("$summary", ready | ||
| ? `Retrieved report for campaign ${this.campaignId}` | ||
| : `Report for campaign ${this.campaignId} is not generated yet`); | ||
| return response; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-get-campaign", | ||
| name: "Get Campaign", | ||
| description: "Retrieve a Darkmoon pentest campaign by ID, including its status, target and the vulnerabilities found so far. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: false, | ||
| readOnlyHint: true, | ||
| }, | ||
| props: { | ||
| app, | ||
| campaignId: { | ||
| propDefinition: [ | ||
| app, | ||
| "campaignId", | ||
| ], | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.getCampaign({ | ||
| $, | ||
| campaignId: this.campaignId, | ||
| }); | ||
| $.export("$summary", `Retrieved campaign ${this.campaignId}`); | ||
| return response; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-get-finding", | ||
| name: "Get Finding", | ||
| description: "Retrieve a single Darkmoon finding (vulnerability) by ID, with its severity, CVSS score, CVE, MITRE ATT&CK mapping, evidence and remediation advice. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: false, | ||
| readOnlyHint: true, | ||
| }, | ||
| props: { | ||
| app, | ||
| findingId: { | ||
| propDefinition: [ | ||
| app, | ||
| "findingId", | ||
| ], | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.getFinding({ | ||
| $, | ||
| findingId: this.findingId, | ||
| }); | ||
| const title = response?.data?.title; | ||
| $.export("$summary", `Retrieved finding ${this.findingId}${title | ||
| ? `: ${title}` | ||
| : ""}`); | ||
| return response; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-launch-campaign", | ||
| name: "Launch Campaign", | ||
| description: "Start an autonomous Darkmoon pentest campaign against a target and return the `run_id` of the run. Only scan systems you are authorized to test. Use **List Campaigns** to follow its progress. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| readOnlyHint: false, | ||
| }, | ||
| props: { | ||
| app, | ||
| target: { | ||
| type: "string", | ||
| label: "Target", | ||
| description: "The host or URL to test, e.g. `https://staging.example.com`. You must be authorized to test it.", | ||
| }, | ||
| outOfScope: { | ||
| type: "string[]", | ||
| label: "Out of Scope", | ||
| description: "Hosts or paths the agents must not touch, e.g. `https://staging.example.com/admin`.", | ||
| optional: true, | ||
| }, | ||
| focus: { | ||
| type: "string[]", | ||
| label: "Focus", | ||
| description: "Vulnerability classes or areas to prioritize, e.g. `sql_injection`, `authentication`.", | ||
| optional: true, | ||
| }, | ||
| noise: { | ||
| type: "string", | ||
| label: "Noise", | ||
| description: "How loud the scan is allowed to be, as understood by your Darkmoon deployment, e.g. `low`.", | ||
| optional: true, | ||
| }, | ||
| safeHarbor: { | ||
| type: "string", | ||
| label: "Safe Harbor", | ||
| description: "Optional safe harbor or authorization statement attached to the run, e.g. `Authorized by the security team, ticket SEC-123`.", | ||
| optional: true, | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.launchCampaign({ | ||
| $, | ||
| data: { | ||
| target: this.target, | ||
| out_of_scope: this.outOfScope, | ||
| focus: this.focus, | ||
| noise: this.noise, | ||
| safe_harbor: this.safeHarbor, | ||
| }, | ||
| }); | ||
| $.export("$summary", `Launched campaign against ${this.target}${response?.run_id | ||
| ? ` (run ${response.run_id})` | ||
| : ""}`); | ||
| return response; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-list-campaigns", | ||
| name: "List Campaigns", | ||
| description: "List Darkmoon pentest campaigns, optionally filtered by target ID or campaign status. Returns each campaign with its target, status and severity counts. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: false, | ||
| readOnlyHint: true, | ||
| }, | ||
| props: { | ||
| app, | ||
| targetId: { | ||
| type: "string", | ||
| label: "Target ID", | ||
| description: "Only return campaigns run against this target ID.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add examples and a source tool to the Neither description includes an example value. As per path instructions: "Flag ID-like props that do not name a source tool". Also applies to: 26-26 🤖 Prompt for AI AgentsSource: Path instructions |
||
| optional: true, | ||
| }, | ||
| status: { | ||
| type: "string", | ||
| label: "Status", | ||
| description: "Only return campaigns with this status.", | ||
| options: [ | ||
| "queued", | ||
| "running", | ||
| "completed", | ||
| "stopped", | ||
| "failed", | ||
| ], | ||
| optional: true, | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.listCampaigns({ | ||
| $, | ||
| params: { | ||
| target_id: this.targetId, | ||
| status: this.status, | ||
| }, | ||
| }); | ||
| const count = response?.data?.length ?? 0; | ||
| $.export("$summary", `Found ${count} campaign${count === 1 | ||
| ? "" | ||
| : "s"}`); | ||
| return response; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import app from "../../darkmoon.app.mjs"; | ||
|
|
||
| export default { | ||
| key: "darkmoon-list-findings", | ||
| name: "List Findings", | ||
| description: "List vulnerabilities (findings) discovered by Darkmoon, optionally filtered by campaign, severity, category or status. Each finding includes title, severity, CVSS score, category, endpoint and remediation advice. Requires a Darkmoon Pro dashboard API. [See the documentation](https://github.com/ASCIT31/Dark-Moon)", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| ai: "optimized", | ||
| annotations: { | ||
| destructiveHint: false, | ||
| openWorldHint: false, | ||
| readOnlyHint: true, | ||
| }, | ||
| props: { | ||
| app, | ||
| campaignId: { | ||
| propDefinition: [ | ||
| app, | ||
| "campaignId", | ||
| ], | ||
| optional: true, | ||
| }, | ||
| severity: { | ||
| propDefinition: [ | ||
| app, | ||
| "severity", | ||
| ], | ||
| }, | ||
| category: { | ||
| type: "string", | ||
| label: "Category", | ||
| description: "Only return findings of this category, e.g. `sql_injection`, `xss_stored`, `ssrf`, `remote_code_execution`.", | ||
| optional: true, | ||
| }, | ||
| status: { | ||
| propDefinition: [ | ||
| app, | ||
| "status", | ||
| ], | ||
| }, | ||
| }, | ||
| async run({ $ }) { | ||
| const response = await this.app.listFindings({ | ||
| $, | ||
| params: { | ||
| campaign_id: this.campaignId, | ||
| severity: this.severity, | ||
| category: this.category, | ||
| status: this.status, | ||
| }, | ||
| }); | ||
| const count = response?.data?.length ?? 0; | ||
| $.export("$summary", `Found ${count} finding${count === 1 | ||
| ? "" | ||
| : "s"}`); | ||
| return response; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { axios } from "@pipedream/platform"; | ||
|
|
||
| export default { | ||
| type: "app", | ||
| app: "darkmoon", | ||
| propDefinitions: { | ||
| campaignId: { | ||
| type: "string", | ||
| label: "Campaign ID", | ||
| description: "The ID of a Darkmoon campaign. Run the **List Campaigns** action first to look up a campaign `id`.", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add concrete example values to the shared prop descriptions. The As per path instructions: "A concrete example value ... required for every prop ... Flag any prop description with no example". Also applies to: 24-24, 29-29, 42-42 🤖 Prompt for AI AgentsSource: Path instructions |
||
| async options() { | ||
| const { data: campaigns = [] } = await this.listCampaigns(); | ||
| return campaigns.map(({ | ||
| id, target, status, | ||
| }) => ({ | ||
| label: `${target || id} (${status || "unknown"})`, | ||
| value: id, | ||
| })); | ||
| }, | ||
| }, | ||
| findingId: { | ||
| type: "string", | ||
| label: "Finding ID", | ||
| description: "The ID of a Darkmoon finding (vulnerability). Run the **List Findings** action first to look up a finding `id`.", | ||
| }, | ||
| severity: { | ||
| type: "string", | ||
| label: "Severity", | ||
| description: "Only return findings with this severity.", | ||
| options: [ | ||
| "critical", | ||
| "high", | ||
| "medium", | ||
| "low", | ||
| "info", | ||
| ], | ||
| optional: true, | ||
| }, | ||
| status: { | ||
| type: "string", | ||
| label: "Status", | ||
| description: "Only return findings with this status. `exploited` and `confirmed` findings were proven by the agents, `unconfirmed` ones were not.", | ||
| options: [ | ||
| "exploited", | ||
| "confirmed", | ||
| "unconfirmed", | ||
| "remediated", | ||
| ], | ||
| optional: true, | ||
| }, | ||
| }, | ||
| methods: { | ||
| _baseUrl() { | ||
| // Accept the server root or the full API root, with or without a trailing slash | ||
| const baseUrl = String(this.$auth.base_url).replace(/\/+$/, "") | ||
| .replace(/\/api\/v1$/, ""); | ||
| return `${baseUrl}/api/v1`; | ||
| }, | ||
| _headers() { | ||
| return { | ||
| Authorization: `Bearer ${this.$auth.api_token}`, | ||
| }; | ||
| }, | ||
| _makeRequest({ | ||
| $ = this, path, ...opts | ||
| }) { | ||
| return axios($, { | ||
| url: `${this._baseUrl()}${path}`, | ||
| headers: this._headers(), | ||
| ...opts, | ||
| }); | ||
| }, | ||
| listCampaigns(opts = {}) { | ||
| return this._makeRequest({ | ||
| path: "/campaigns", | ||
| ...opts, | ||
| }); | ||
| }, | ||
| getCampaign({ | ||
| campaignId, ...opts | ||
| }) { | ||
| return this._makeRequest({ | ||
| path: `/campaigns/${encodeURIComponent(campaignId)}`, | ||
| ...opts, | ||
| }); | ||
| }, | ||
| getCampaignReport({ | ||
| campaignId, ...opts | ||
| }) { | ||
| return this._makeRequest({ | ||
| path: `/campaigns/${encodeURIComponent(campaignId)}/report`, | ||
| ...opts, | ||
| }); | ||
| }, | ||
| listFindings(opts = {}) { | ||
| return this._makeRequest({ | ||
| path: "/vulnerabilities", | ||
| ...opts, | ||
| }); | ||
| }, | ||
| getFinding({ | ||
| findingId, ...opts | ||
| }) { | ||
| return this._makeRequest({ | ||
| path: `/vulnerabilities/${encodeURIComponent(findingId)}`, | ||
| ...opts, | ||
| }); | ||
| }, | ||
| launchCampaign(opts = {}) { | ||
| return this._makeRequest({ | ||
| method: "POST", | ||
| path: "/run/campaign", | ||
| ...opts, | ||
| }); | ||
| }, | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Set
openWorldHint: trueon all read actions. Each of these read actions calls the external Darkmoon API. Each one declaresopenWorldHint: false, which is incorrect for an action that makes external API calls.components/darkmoon/actions/list-campaigns/list-campaigns.mjs#L12-L12: change the value toopenWorldHint: true.components/darkmoon/actions/get-campaign/get-campaign.mjs#L12-L12: change the value toopenWorldHint: true.components/darkmoon/actions/get-campaign-report/get-campaign-report.mjs#L12-L12: change the value toopenWorldHint: true.components/darkmoon/actions/list-findings/list-findings.mjs#L12-L12: change the value toopenWorldHint: true.components/darkmoon/actions/get-finding/get-finding.mjs#L12-L12: change the value toopenWorldHint: true.As per path instructions: "
openWorldHint: truefor any action making external API calls".📍 Affects 5 files
components/darkmoon/actions/list-campaigns/list-campaigns.mjs#L12-L12(this comment)components/darkmoon/actions/get-campaign/get-campaign.mjs#L12-L12components/darkmoon/actions/get-campaign-report/get-campaign-report.mjs#L12-L12components/darkmoon/actions/list-findings/list-findings.mjs#L12-L12components/darkmoon/actions/get-finding/get-finding.mjs#L12-L12🤖 Prompt for AI Agents
Source: Path instructions