lfx-darkmoon 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ venv/
7
+ .env
8
+ .env.*
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ASC-IT (Darkmoon)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: lfx-darkmoon
3
+ Version: 0.1.0
4
+ Summary: Darkmoon components as a standalone Langflow Extension Bundle: run an authorised AI pentest, pull findings, and review the fix pull requests Darkmoon prepares.
5
+ Project-URL: Homepage, https://github.com/ASCIT31/Dark-Moon
6
+ Project-URL: Documentation, https://docs.langflow.org/extensions
7
+ Project-URL: Source, https://github.com/ASCIT31/langflow-darkmoon
8
+ Project-URL: Issues, https://github.com/ASCIT31/langflow-darkmoon/issues
9
+ Author: ASC-IT (Darkmoon)
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-security,appsec,bundle,darkmoon,dast,extension,langflow,lfx,pentest,security
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Security
18
+ Requires-Python: <3.15,>=3.10
19
+ Requires-Dist: lfx<2.0.0,>=1.12.0
20
+ Requires-Dist: requests>=2.28
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=8; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # langflow-darkmoon
26
+
27
+ [![Darkmoon](https://img.shields.io/badge/Darkmoon-autonomous%20pentest-4f46e5)](https://github.com/ASCIT31/Dark-Moon)
28
+ [![Star Dark-Moon on GitHub](https://img.shields.io/github/stars/ASCIT31/Dark-Moon?style=social)](https://github.com/ASCIT31/Dark-Moon)
29
+
30
+ A [Langflow](https://github.com/langflow-ai/langflow) **Extension Bundle** (distribution `lfx-darkmoon`) for **[Darkmoon](https://github.com/ASCIT31/Dark-Moon)**, the local, privacy first autonomous AI penetration testing engine (GPLv3). Drop these components into a Langflow flow to **trigger a Darkmoon pentest against a target you are authorised to assess, pull back the findings, and review the fix pull requests Darkmoon prepares**, over your own self-hosted Darkmoon Dashboard API.
31
+
32
+ > Darkmoon runs and validates security tests. It does not, and these components do not, guarantee that a system is secure. Findings can include false positives and must be reviewed by a qualified human. Only run assessments against systems you own or have explicit written authorisation to test. These components never merge a pull request; every fix is left for a person to review and merge.
33
+
34
+ ## Components
35
+
36
+ | Component | Purpose |
37
+ | --- | --- |
38
+ | **Darkmoon Run Pentest** | Start a campaign against an authorised target; optionally wait and return findings and severity stats. |
39
+ | **Darkmoon Get Findings** | Return the vulnerabilities and aggregated stats for a campaign id. |
40
+ | **Darkmoon List Campaigns** | List the campaigns visible to the authenticated dashboard user. |
41
+ | **Darkmoon List Pull Requests** | List the fix pull requests Darkmoon prepared (read only), optionally scoped and filtered by state. |
42
+
43
+ The bundle registers under the `darkmoon` group. Each component is addressable by the canonical namespaced id `ext:darkmoon:<Class>@official`, for example `ext:darkmoon:DarkmoonRunPentest@official`.
44
+
45
+ ## Install
46
+
47
+ Darkmoon is self hosted, so you point the components at your own **Darkmoon Dashboard API** (the FastAPI service shipped with Darkmoon, typically port `8000`). There is no public endpoint. Each component logs in with `POST /api/v1/auth/login` to obtain a short lived JWT.
48
+
49
+ This is a standalone [Langflow Extension Bundle](https://docs.langflow.org/extensions): install it alongside Langflow and it is discovered automatically through the `langflow.extensions` entry point. No fork of Langflow and no `LANGFLOW_COMPONENTS_PATH` are required.
50
+
51
+ ```bash
52
+ pip install langflow lfx-darkmoon
53
+ langflow run
54
+ ```
55
+
56
+ Restart the Langflow server; the four Darkmoon components appear in the visual editor under the **Darkmoon** bundle group. Set the base URL, username and password on a component, and a target on **Darkmoon Run Pentest**.
57
+
58
+ ## Remediation and secrets (Pro)
59
+
60
+ The Darkmoon dashboard and the remediation feature that prepares fix pull requests are paid **Pro** capabilities. The open source Darkmoon (the CLI engine) focuses on finding, proving and reporting findings locally. When remediation is enabled it needs a **credential reference**, an opaque id of a credential stored in Darkmoon's encrypted vault (created in the dashboard), not a raw token. Raw source control secrets never travel through these components, and the remediation agent only ever prepares a pull request for human review; it never merges. **Darkmoon List Pull Requests** can read those pull requests through the API.
61
+
62
+ ## Privacy
63
+
64
+ Darkmoon keeps assessment work on your own infrastructure and applies a privacy gateway so the language model works over placeholders in place of your real hosts, IPs and credentials. These components send data only to the base URL you configure.
65
+
66
+ ## Darkmoon at a glance
67
+
68
+ Darkmoon is a GPLv3, self hosted autonomous AI pentester: an LLM orchestrates specialist agents and offensive tools over MCP, runs on a local model, and proves each finding with a real exploit. See [ASCIT31/Dark-Moon](https://github.com/ASCIT31/Dark-Moon).
69
+
70
+ ## Layout
71
+
72
+ ```
73
+ langflow-darkmoon/
74
+ ├── pyproject.toml # distribution: lfx-darkmoon
75
+ ├── src/
76
+ │ └── lfx_darkmoon/
77
+ │ ├── __init__.py # re-exports the four components
78
+ │ ├── extension.json # Extension Bundle manifest (v1)
79
+ │ └── components/
80
+ │ └── darkmoon/ # the bundle (path in extension.json)
81
+ │ ├── run_pentest.py
82
+ │ ├── get_findings.py
83
+ │ ├── list_campaigns.py
84
+ │ ├── list_pull_requests.py
85
+ │ ├── _base.py # shared inputs + client mixin (not a Component)
86
+ │ ├── darkmoon_client.py# dependency free, transport injected API client
87
+ │ └── transport.py # requests transport + client factory
88
+ └── tests/
89
+ ```
90
+
91
+ ## Development
92
+
93
+ ```bash
94
+ pip install langflow # brings lfx (the Bundle API surface)
95
+ pip install -e ".[test]" # install this bundle editable
96
+ lfx extension validate src/lfx_darkmoon # static manifest + bundle check
97
+ lfx extension list # confirm it is discovered at @official
98
+ pytest -q
99
+ ```
100
+
101
+ The API client (`darkmoon_client.py`) is dependency free and transport injected, so its logic is unit tested without a network (mock transport). The component tests instantiate the real Langflow (`lfx`) components, inject a mock transport, run each output, and assert each builds a valid Langflow frontend node. A full end to end run additionally requires a running Darkmoon instance pointed at an authorised target.
102
+
103
+ Intra bundle imports are absolute (`from lfx_darkmoon.components.darkmoon...`), as required by the Langflow bundle loader, which does not support relative imports between bundle modules.
104
+
105
+ ## Related integrations
106
+
107
+ Darkmoon also ships integrations for [n8n](https://github.com/ASCIT31/n8n-nodes-darkmoon), [Activepieces](https://github.com/ASCIT31/activepieces-piece-darkmoon), [Dify](https://github.com/ASCIT31/dify-plugin-darkmoon), [LangChain](https://github.com/ASCIT31/langchain-darkmoon) and [GitHub Actions](https://github.com/ASCIT31/darkmoon-scan-action).
108
+
109
+ ## License
110
+
111
+ MIT, see [LICENSE](./LICENSE). Not affiliated with Langflow; "Langflow" is a trademark of its respective owner.
@@ -0,0 +1,87 @@
1
+ # langflow-darkmoon
2
+
3
+ [![Darkmoon](https://img.shields.io/badge/Darkmoon-autonomous%20pentest-4f46e5)](https://github.com/ASCIT31/Dark-Moon)
4
+ [![Star Dark-Moon on GitHub](https://img.shields.io/github/stars/ASCIT31/Dark-Moon?style=social)](https://github.com/ASCIT31/Dark-Moon)
5
+
6
+ A [Langflow](https://github.com/langflow-ai/langflow) **Extension Bundle** (distribution `lfx-darkmoon`) for **[Darkmoon](https://github.com/ASCIT31/Dark-Moon)**, the local, privacy first autonomous AI penetration testing engine (GPLv3). Drop these components into a Langflow flow to **trigger a Darkmoon pentest against a target you are authorised to assess, pull back the findings, and review the fix pull requests Darkmoon prepares**, over your own self-hosted Darkmoon Dashboard API.
7
+
8
+ > Darkmoon runs and validates security tests. It does not, and these components do not, guarantee that a system is secure. Findings can include false positives and must be reviewed by a qualified human. Only run assessments against systems you own or have explicit written authorisation to test. These components never merge a pull request; every fix is left for a person to review and merge.
9
+
10
+ ## Components
11
+
12
+ | Component | Purpose |
13
+ | --- | --- |
14
+ | **Darkmoon Run Pentest** | Start a campaign against an authorised target; optionally wait and return findings and severity stats. |
15
+ | **Darkmoon Get Findings** | Return the vulnerabilities and aggregated stats for a campaign id. |
16
+ | **Darkmoon List Campaigns** | List the campaigns visible to the authenticated dashboard user. |
17
+ | **Darkmoon List Pull Requests** | List the fix pull requests Darkmoon prepared (read only), optionally scoped and filtered by state. |
18
+
19
+ The bundle registers under the `darkmoon` group. Each component is addressable by the canonical namespaced id `ext:darkmoon:<Class>@official`, for example `ext:darkmoon:DarkmoonRunPentest@official`.
20
+
21
+ ## Install
22
+
23
+ Darkmoon is self hosted, so you point the components at your own **Darkmoon Dashboard API** (the FastAPI service shipped with Darkmoon, typically port `8000`). There is no public endpoint. Each component logs in with `POST /api/v1/auth/login` to obtain a short lived JWT.
24
+
25
+ This is a standalone [Langflow Extension Bundle](https://docs.langflow.org/extensions): install it alongside Langflow and it is discovered automatically through the `langflow.extensions` entry point. No fork of Langflow and no `LANGFLOW_COMPONENTS_PATH` are required.
26
+
27
+ ```bash
28
+ pip install langflow lfx-darkmoon
29
+ langflow run
30
+ ```
31
+
32
+ Restart the Langflow server; the four Darkmoon components appear in the visual editor under the **Darkmoon** bundle group. Set the base URL, username and password on a component, and a target on **Darkmoon Run Pentest**.
33
+
34
+ ## Remediation and secrets (Pro)
35
+
36
+ The Darkmoon dashboard and the remediation feature that prepares fix pull requests are paid **Pro** capabilities. The open source Darkmoon (the CLI engine) focuses on finding, proving and reporting findings locally. When remediation is enabled it needs a **credential reference**, an opaque id of a credential stored in Darkmoon's encrypted vault (created in the dashboard), not a raw token. Raw source control secrets never travel through these components, and the remediation agent only ever prepares a pull request for human review; it never merges. **Darkmoon List Pull Requests** can read those pull requests through the API.
37
+
38
+ ## Privacy
39
+
40
+ Darkmoon keeps assessment work on your own infrastructure and applies a privacy gateway so the language model works over placeholders in place of your real hosts, IPs and credentials. These components send data only to the base URL you configure.
41
+
42
+ ## Darkmoon at a glance
43
+
44
+ Darkmoon is a GPLv3, self hosted autonomous AI pentester: an LLM orchestrates specialist agents and offensive tools over MCP, runs on a local model, and proves each finding with a real exploit. See [ASCIT31/Dark-Moon](https://github.com/ASCIT31/Dark-Moon).
45
+
46
+ ## Layout
47
+
48
+ ```
49
+ langflow-darkmoon/
50
+ ├── pyproject.toml # distribution: lfx-darkmoon
51
+ ├── src/
52
+ │ └── lfx_darkmoon/
53
+ │ ├── __init__.py # re-exports the four components
54
+ │ ├── extension.json # Extension Bundle manifest (v1)
55
+ │ └── components/
56
+ │ └── darkmoon/ # the bundle (path in extension.json)
57
+ │ ├── run_pentest.py
58
+ │ ├── get_findings.py
59
+ │ ├── list_campaigns.py
60
+ │ ├── list_pull_requests.py
61
+ │ ├── _base.py # shared inputs + client mixin (not a Component)
62
+ │ ├── darkmoon_client.py# dependency free, transport injected API client
63
+ │ └── transport.py # requests transport + client factory
64
+ └── tests/
65
+ ```
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ pip install langflow # brings lfx (the Bundle API surface)
71
+ pip install -e ".[test]" # install this bundle editable
72
+ lfx extension validate src/lfx_darkmoon # static manifest + bundle check
73
+ lfx extension list # confirm it is discovered at @official
74
+ pytest -q
75
+ ```
76
+
77
+ The API client (`darkmoon_client.py`) is dependency free and transport injected, so its logic is unit tested without a network (mock transport). The component tests instantiate the real Langflow (`lfx`) components, inject a mock transport, run each output, and assert each builds a valid Langflow frontend node. A full end to end run additionally requires a running Darkmoon instance pointed at an authorised target.
78
+
79
+ Intra bundle imports are absolute (`from lfx_darkmoon.components.darkmoon...`), as required by the Langflow bundle loader, which does not support relative imports between bundle modules.
80
+
81
+ ## Related integrations
82
+
83
+ Darkmoon also ships integrations for [n8n](https://github.com/ASCIT31/n8n-nodes-darkmoon), [Activepieces](https://github.com/ASCIT31/activepieces-piece-darkmoon), [Dify](https://github.com/ASCIT31/dify-plugin-darkmoon), [LangChain](https://github.com/ASCIT31/langchain-darkmoon) and [GitHub Actions](https://github.com/ASCIT31/darkmoon-scan-action).
84
+
85
+ ## License
86
+
87
+ MIT, see [LICENSE](./LICENSE). Not affiliated with Langflow; "Langflow" is a trademark of its respective owner.
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "lfx-darkmoon"
3
+ version = "0.1.0"
4
+ description = "Darkmoon components as a standalone Langflow Extension Bundle: run an authorised AI pentest, pull findings, and review the fix pull requests Darkmoon prepares."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.15"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "ASC-IT (Darkmoon)" }]
9
+ keywords = ["langflow", "lfx", "extension", "bundle", "darkmoon", "pentest", "security", "dast", "appsec", "ai-security"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Topic :: Security",
16
+ ]
17
+
18
+ # Runtime deps: lfx (the public BUNDLE_API surface these components are built
19
+ # against) and requests (the HTTP transport). lfx is floored at the current
20
+ # major.minor line and capped below the next lfx major; fine-grained BUNDLE_API
21
+ # compatibility is enforced separately via extension.json's
22
+ # "lfx": {"compat": ["1"]} contract against the running lfx's BUNDLE_API_VERSION.
23
+ dependencies = [
24
+ "lfx>=1.12.0,<2.0.0",
25
+ "requests>=2.28",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ test = ["pytest>=8"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/ASCIT31/Dark-Moon"
33
+ Documentation = "https://docs.langflow.org/extensions"
34
+ Source = "https://github.com/ASCIT31/langflow-darkmoon"
35
+ Issues = "https://github.com/ASCIT31/langflow-darkmoon/issues"
36
+
37
+ [build-system]
38
+ requires = ["hatchling>=1.31.0"]
39
+ build-backend = "hatchling.build"
40
+
41
+ # Manifest-shipping distributions are discovered via the ``langflow.extensions``
42
+ # entry-point. The value is the dotted path to the package containing
43
+ # ``extension.json``; the loader walks ``importlib.metadata.files()`` from
44
+ # there to find the manifest, and falls back to this entry-point for editable
45
+ # installs whose dist.files only surfaces dist-info entries.
46
+ [project.entry-points."langflow.extensions"]
47
+ lfx-darkmoon = "lfx_darkmoon"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ # extension.json + components live inside the lfx_darkmoon package so
51
+ # ``importlib.metadata.files(dist)`` finds them and the loader resolves
52
+ # bundles[].path relative to the manifest's directory.
53
+ packages = ["src/lfx_darkmoon"]
54
+ include = [
55
+ "src/lfx_darkmoon/extension.json",
56
+ "src/lfx_darkmoon/components/**/*.py",
57
+ "src/lfx_darkmoon/components/**/*.svg",
58
+ ]
59
+
60
+ [tool.hatch.build.targets.sdist]
61
+ include = [
62
+ "src/lfx_darkmoon",
63
+ "README.md",
64
+ "pyproject.toml",
65
+ ]
@@ -0,0 +1,25 @@
1
+ """lfx-darkmoon: Darkmoon components as a standalone Langflow Extension Bundle.
2
+
3
+ This package is the distribution unit ``lfx-darkmoon``. At runtime Langflow's
4
+ loader discovers the ``extension.json`` shipped alongside this ``__init__.py``
5
+ and registers the four Darkmoon components under namespaced IDs such as
6
+ ``ext:darkmoon:DarkmoonRunPentest@official``.
7
+
8
+ Darkmoon is the local, privacy-first autonomous AI penetration testing engine
9
+ (GPLv3): https://github.com/ASCIT31/Dark-Moon. These components talk to a
10
+ self-hosted Darkmoon Dashboard API; there is no public hosted endpoint.
11
+ """
12
+
13
+ from lfx_darkmoon.components.darkmoon import (
14
+ DarkmoonGetFindingsComponent,
15
+ DarkmoonListCampaignsComponent,
16
+ DarkmoonListPullRequestsComponent,
17
+ DarkmoonRunPentestComponent,
18
+ )
19
+
20
+ __all__ = [
21
+ "DarkmoonRunPentestComponent",
22
+ "DarkmoonGetFindingsComponent",
23
+ "DarkmoonListCampaignsComponent",
24
+ "DarkmoonListPullRequestsComponent",
25
+ ]
@@ -0,0 +1,23 @@
1
+ """Darkmoon bundle for Langflow.
2
+
3
+ The ``darkmoon`` bundle of the ``lfx-darkmoon`` Extension Bundle. It ships four
4
+ components: Run Pentest, Get Findings, List Campaigns and List Pull Requests,
5
+ over a self-hosted Darkmoon Dashboard API. Langflow discovers them via the
6
+ manifest at ``lfx_darkmoon/extension.json``; each registers under the namespaced
7
+ id ``ext:darkmoon:<Class>@official``. See https://github.com/ASCIT31/Dark-Moon.
8
+ """
9
+ from lfx_darkmoon.components.darkmoon.darkmoon_client import DarkmoonClient, DarkmoonError, PR_STATES
10
+ from lfx_darkmoon.components.darkmoon.get_findings import DarkmoonGetFindingsComponent
11
+ from lfx_darkmoon.components.darkmoon.list_campaigns import DarkmoonListCampaignsComponent
12
+ from lfx_darkmoon.components.darkmoon.list_pull_requests import DarkmoonListPullRequestsComponent
13
+ from lfx_darkmoon.components.darkmoon.run_pentest import DarkmoonRunPentestComponent
14
+
15
+ __all__ = [
16
+ "DarkmoonClient",
17
+ "DarkmoonError",
18
+ "PR_STATES",
19
+ "DarkmoonRunPentestComponent",
20
+ "DarkmoonGetFindingsComponent",
21
+ "DarkmoonListCampaignsComponent",
22
+ "DarkmoonListPullRequestsComponent",
23
+ ]
@@ -0,0 +1,52 @@
1
+ """Shared connection inputs and client helper for the Darkmoon Langflow components."""
2
+ from __future__ import annotations
3
+
4
+ from lfx.io import SecretStrInput, StrInput
5
+
6
+ from lfx_darkmoon.components.darkmoon.darkmoon_client import DarkmoonClient
7
+ from lfx_darkmoon.components.darkmoon.transport import logged_in_client
8
+
9
+
10
+ def connection_inputs():
11
+ """Fresh connection input instances (base URL / username / password).
12
+
13
+ Darkmoon is self-hosted, so the base URL is always supplied by the operator;
14
+ there is no public hosted endpoint.
15
+ """
16
+ return [
17
+ StrInput(
18
+ name="base_url",
19
+ display_name="Darkmoon base URL",
20
+ info="Base URL of your self-hosted Darkmoon Dashboard API (FastAPI, typically port 8000).",
21
+ required=True,
22
+ ),
23
+ StrInput(
24
+ name="username",
25
+ display_name="Dashboard username",
26
+ info="A Darkmoon dashboard user.",
27
+ required=True,
28
+ ),
29
+ SecretStrInput(
30
+ name="password",
31
+ display_name="Dashboard password",
32
+ info="The dashboard user's password. Used only to obtain a short-lived JWT.",
33
+ required=True,
34
+ ),
35
+ ]
36
+
37
+
38
+ class DarkmoonClientMixin:
39
+ """Mixin that builds an authenticated Darkmoon client per run.
40
+
41
+ This is deliberately NOT a ``Component`` subclass: each concrete component
42
+ inherits ``(DarkmoonClientMixin, Component)`` so the extension validator
43
+ sees exactly the four real palette components and no abstract base without a
44
+ ``build()``/output method.
45
+
46
+ Tests monkeypatch :func:`logged_in_client` (a module global in this file) to
47
+ inject a mock-transport client, so component logic is exercised without a
48
+ network.
49
+ """
50
+
51
+ def _client(self) -> DarkmoonClient:
52
+ return logged_in_client(self.base_url, self.username, self.password)
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Darkmoon">
2
+ <defs>
3
+ <linearGradient id="dm" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0" stop-color="#312e81"/>
5
+ <stop offset="0.55" stop-color="#4f46e5"/>
6
+ <stop offset="1" stop-color="#eff6ff"/>
7
+ </linearGradient>
8
+ </defs>
9
+ <rect width="64" height="64" rx="12" fill="#0b1020"/>
10
+ <g fill="url(#dm)">
11
+ <rect x="12" y="12" width="10" height="10" rx="2"/>
12
+ <rect x="26" y="12" width="10" height="10" rx="2"/>
13
+ <rect x="12" y="26" width="10" height="10" rx="2"/>
14
+ <rect x="40" y="26" width="10" height="10" rx="2"/>
15
+ <rect x="26" y="40" width="10" height="10" rx="2"/>
16
+ <rect x="40" y="40" width="10" height="10" rx="2"/>
17
+ <rect x="12" y="40" width="10" height="10" rx="2"/>
18
+ </g>
19
+ <circle cx="45" cy="19" r="7" fill="#eff6ff"/>
20
+ <circle cx="48.5" cy="17" r="6" fill="#0b1020"/>
21
+ </svg>
@@ -0,0 +1,348 @@
1
+ """
2
+ DarkmoonClient - dependency-free client for the Darkmoon Dashboard API.
3
+
4
+ This module deliberately has NO Dify or third-party imports. It receives an
5
+ injected HTTP function so it can run both inside a Dify tool (wrapping
6
+ ``requests``) and in a plain test harness (wrapping a mock). This keeps the
7
+ component logic free of heavy runtime dependencies and lets the integration be
8
+ unit-tested end to end without a live Darkmoon instance.
9
+
10
+ The Darkmoon Dashboard API is a versioned FastAPI service (self-hosted). The
11
+ endpoints used here mirror the ones the maintained n8n and Activepieces
12
+ Darkmoon integrations use, read from Dark-Moon-Front-API (branch dev):
13
+
14
+ POST /api/v1/auth/login -> { token, ... }
15
+ POST /api/v1/run/campaign -> { run_id, pid, command }
16
+ GET /api/v1/run/logs/{run_id} -> { data: events[], total }
17
+ DELETE /api/v1/run/{run_id}/stop -> { message, run_id }
18
+ GET /api/v1/campaigns -> { data: campaigns[], total }
19
+ GET /api/v1/vulnerabilities?campaign_id -> { data, total, stats }
20
+ GET /api/v1/campaigns/{id}/report -> { content, format }
21
+ GET /api/v1/pull-requests?campaign_id -> { data: PullRequest[], total }
22
+
23
+ There is no public hosted endpoint; every deployment is self-hosted, so the
24
+ base URL is always supplied by the operator.
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import time
29
+ from dataclasses import dataclass
30
+ from typing import Any, Callable, Dict, List, Optional
31
+ from urllib.parse import quote
32
+
33
+ # The exact PR states the store recognises (pr_store.PR_STATES).
34
+ PR_STATES = ("proposed", "draft", "open", "merged", "closed", "error")
35
+
36
+ # Run-log event types that mark a run as finished.
37
+ _TERMINAL_TYPES = frozenset({"run_completed", "run_error"})
38
+
39
+
40
+ @dataclass
41
+ class HttpResponse:
42
+ """Raw HTTP result the injected transport must return."""
43
+
44
+ status_code: int
45
+ body: Any
46
+
47
+
48
+ # Injected transport. Must return the raw status and parsed JSON body, and must
49
+ # NEVER raise on a non-2xx status - the client inspects status_code itself so
50
+ # error messages carry the API's own ``detail`` (and never a local secret).
51
+ HttpFn = Callable[[Dict[str, Any]], HttpResponse]
52
+
53
+
54
+ class DarkmoonError(Exception):
55
+ """Error carrying the API's own detail plus the HTTP status code."""
56
+
57
+ def __init__(self, message: str, status_code: Optional[int] = None) -> None:
58
+ super().__init__(message)
59
+ self.status_code = status_code
60
+
61
+
62
+ def _detail(res: HttpResponse, fallback: str) -> str:
63
+ body = res.body if res else None
64
+ if isinstance(body, dict):
65
+ d = body.get("detail", body.get("message"))
66
+ if isinstance(d, str) and d:
67
+ return d
68
+ return fallback
69
+
70
+
71
+ def _data_list(res: HttpResponse) -> List[Any]:
72
+ body = res.body
73
+ if isinstance(body, dict) and isinstance(body.get("data"), list):
74
+ return body["data"]
75
+ return []
76
+
77
+
78
+ class DarkmoonClient:
79
+ """A thin, transport-injected client over the Darkmoon Dashboard API."""
80
+
81
+ def __init__(self, base_url: str, http: HttpFn) -> None:
82
+ # Strip trailing slashes so path joins stay clean.
83
+ self.base_url = base_url.rstrip("/")
84
+ self._http = http
85
+ self._token: Optional[str] = None
86
+
87
+ def _url(self, path: str) -> str:
88
+ return f"{self.base_url}{path}"
89
+
90
+ def _auth_headers(self) -> Dict[str, str]:
91
+ h: Dict[str, str] = {"Content-Type": "application/json"}
92
+ if self._token:
93
+ h["Authorization"] = f"Bearer {self._token}"
94
+ return h
95
+
96
+ # -- auth ---------------------------------------------------------------
97
+ def login(self, username: str, password: str) -> str:
98
+ """Authenticate and cache the JWT for subsequent calls."""
99
+ res = self._http(
100
+ {
101
+ "method": "POST",
102
+ "url": self._url("/api/v1/auth/login"),
103
+ "headers": {"Content-Type": "application/json"},
104
+ "body": {"username": username, "password": password},
105
+ }
106
+ )
107
+ token = res.body.get("token") if isinstance(res.body, dict) else None
108
+ if res.status_code != 200 or not token:
109
+ raise DarkmoonError(_detail(res, "Darkmoon login failed"), res.status_code)
110
+ self._token = token
111
+ return token
112
+
113
+ # -- campaigns ----------------------------------------------------------
114
+ def list_campaigns(self) -> List[Dict[str, Any]]:
115
+ res = self._http(
116
+ {"method": "GET", "url": self._url("/api/v1/campaigns"), "headers": self._auth_headers()}
117
+ )
118
+ if res.status_code != 200:
119
+ raise DarkmoonError(_detail(res, "Failed to list campaigns"), res.status_code)
120
+ return _data_list(res)
121
+
122
+ # -- run ----------------------------------------------------------------
123
+ def run_campaign(self, params: Dict[str, Any]) -> Dict[str, Any]:
124
+ """Start a pentest in the background. Returns the run handle."""
125
+ res = self._http(
126
+ {
127
+ "method": "POST",
128
+ "url": self._url("/api/v1/run/campaign"),
129
+ "headers": self._auth_headers(),
130
+ "body": params,
131
+ }
132
+ )
133
+ run_id = res.body.get("run_id") if isinstance(res.body, dict) else None
134
+ if res.status_code >= 400 or not run_id:
135
+ raise DarkmoonError(_detail(res, "Failed to start pentest run"), res.status_code)
136
+ return res.body
137
+
138
+ def get_run_log(self, run_id: str) -> List[Dict[str, Any]]:
139
+ res = self._http(
140
+ {
141
+ "method": "GET",
142
+ "url": self._url(f"/api/v1/run/logs/{quote(run_id, safe='')}"),
143
+ "headers": self._auth_headers(),
144
+ }
145
+ )
146
+ if res.status_code == 404:
147
+ return []
148
+ if res.status_code != 200:
149
+ raise DarkmoonError(_detail(res, "Failed to read run log"), res.status_code)
150
+ return _data_list(res)
151
+
152
+ def stop_run(self, run_id: str) -> None:
153
+ self._http(
154
+ {
155
+ "method": "DELETE",
156
+ "url": self._url(f"/api/v1/run/{quote(run_id, safe='')}/stop"),
157
+ "headers": self._auth_headers(),
158
+ }
159
+ )
160
+
161
+ def wait_for_run(
162
+ self,
163
+ run_id: str,
164
+ poll_s: float = 3.0,
165
+ timeout_s: float = 30 * 60,
166
+ on_event: Optional[Callable[[Dict[str, Any]], None]] = None,
167
+ sleep: Optional[Callable[[float], None]] = None,
168
+ ) -> Dict[str, Any]:
169
+ """Poll the run's JSONL log until a terminal event or the timeout.
170
+
171
+ The log is the API's single source of truth for run state: no terminal
172
+ event means the run is still going. Never loops forever.
173
+ """
174
+ _sleep = sleep or time.sleep
175
+ start = time.monotonic()
176
+ seen = 0
177
+ while time.monotonic() - start < timeout_s:
178
+ events = self.get_run_log(run_id)
179
+ if on_event:
180
+ for e in events[seen:]:
181
+ on_event(e)
182
+ seen = len(events)
183
+ terminal = next((e for e in events if e.get("type") in _TERMINAL_TYPES), None)
184
+ if terminal is not None:
185
+ return {"events": events, "terminal": terminal, "timed_out": False}
186
+ _sleep(poll_s)
187
+ events = self.get_run_log(run_id)
188
+ return {"events": events, "terminal": None, "timed_out": True}
189
+
190
+ def resolve_run_campaign(
191
+ self, before_ids: "set[str]", target_host: str
192
+ ) -> Optional[Dict[str, Any]]:
193
+ """Resolve the campaign produced by a run.
194
+
195
+ The trigger returns a run_id, not a campaign_id - the agent mints the
196
+ campaign inside the run. We diff the campaign set captured before the
197
+ run against the set after it, preferring a new campaign whose id matches
198
+ the target host, and fall back to the newest campaign otherwise.
199
+ """
200
+ campaigns = self.list_campaigns()
201
+ host = (target_host or "").strip().lower()
202
+
203
+ def by_date(c: Dict[str, Any]) -> str:
204
+ return str(c.get("date") or "")
205
+
206
+ fresh = [c for c in campaigns if c.get("id") not in before_ids]
207
+ if len(fresh) == 1:
208
+ return fresh[0]
209
+ if len(fresh) > 1:
210
+ fresh.sort(key=by_date, reverse=True)
211
+ if host:
212
+ for c in fresh:
213
+ if host in str(c.get("id", "")).lower():
214
+ return c
215
+ return fresh[0]
216
+ if not campaigns:
217
+ return None
218
+ return sorted(campaigns, key=by_date, reverse=True)[0]
219
+
220
+ # -- findings / report --------------------------------------------------
221
+ def get_findings(self, campaign_id: str) -> Dict[str, Any]:
222
+ res = self._http(
223
+ {
224
+ "method": "GET",
225
+ "url": self._url(
226
+ f"/api/v1/vulnerabilities?campaign_id={quote(campaign_id, safe='')}"
227
+ ),
228
+ "headers": self._auth_headers(),
229
+ }
230
+ )
231
+ if res.status_code != 200:
232
+ raise DarkmoonError(_detail(res, "Failed to fetch findings"), res.status_code)
233
+ body = res.body if isinstance(res.body, dict) else {}
234
+ return {
235
+ "data": body.get("data") or [],
236
+ "total": body.get("total") or 0,
237
+ "stats": body.get("stats") or {},
238
+ }
239
+
240
+ def get_report(self, campaign_id: str) -> Dict[str, Any]:
241
+ res = self._http(
242
+ {
243
+ "method": "GET",
244
+ "url": self._url(f"/api/v1/campaigns/{quote(campaign_id, safe='')}/report"),
245
+ "headers": self._auth_headers(),
246
+ }
247
+ )
248
+ if res.status_code != 200:
249
+ raise DarkmoonError(_detail(res, "Failed to fetch report"), res.status_code)
250
+ body = res.body if isinstance(res.body, dict) else {}
251
+ return {"content": body.get("content") or "", "format": body.get("format") or "markdown"}
252
+
253
+ # -- pull requests (read-only) -----------------------------------------
254
+ def list_pull_requests(self, campaign_id: Optional[str] = None) -> List[Dict[str, Any]]:
255
+ """List fix pull requests. campaign_id is the ONLY server-side filter."""
256
+ q = f"?campaign_id={quote(campaign_id, safe='')}" if campaign_id else ""
257
+ res = self._http(
258
+ {
259
+ "method": "GET",
260
+ "url": self._url(f"/api/v1/pull-requests{q}"),
261
+ "headers": self._auth_headers(),
262
+ }
263
+ )
264
+ if res.status_code != 200:
265
+ raise DarkmoonError(_detail(res, "Failed to list pull requests"), res.status_code)
266
+ return _data_list(res)
267
+
268
+ def get_pull_request(self, pr_id: str) -> Dict[str, Any]:
269
+ res = self._http(
270
+ {
271
+ "method": "GET",
272
+ "url": self._url(f"/api/v1/pull-requests/{quote(pr_id, safe='')}"),
273
+ "headers": self._auth_headers(),
274
+ }
275
+ )
276
+ if res.status_code != 200:
277
+ raise DarkmoonError(_detail(res, f"Pull request {pr_id} not found"), res.status_code)
278
+ body = res.body if isinstance(res.body, dict) else {}
279
+ return body.get("data") or {}
280
+
281
+ def get_pull_requests_for_finding(self, vuln_id: str) -> List[Dict[str, Any]]:
282
+ res = self._http(
283
+ {
284
+ "method": "GET",
285
+ "url": self._url(f"/api/v1/pull-requests/finding/{quote(vuln_id, safe='')}"),
286
+ "headers": self._auth_headers(),
287
+ }
288
+ )
289
+ if res.status_code != 200:
290
+ raise DarkmoonError(
291
+ _detail(res, f"Failed to fetch pull requests for finding {vuln_id}"),
292
+ res.status_code,
293
+ )
294
+ return _data_list(res)
295
+
296
+ @staticmethod
297
+ def filter_pull_requests(
298
+ prs: List[Dict[str, Any]],
299
+ state: Optional[List[str]] = None,
300
+ provider: Optional[str] = None,
301
+ repository: Optional[str] = None,
302
+ ) -> List[Dict[str, Any]]:
303
+ """Client-side narrowing. The API does not filter by these fields."""
304
+ out = prs
305
+ if state:
306
+ want = {s.lower() for s in state}
307
+ out = [p for p in out if str(p.get("state", "")).lower() in want]
308
+ if provider:
309
+ p = provider.lower()
310
+ out = [x for x in out if str(x.get("provider", "")).lower() == p]
311
+ if repository:
312
+ r = repository.lower()
313
+ out = [x for x in out if r in str(x.get("repo", "")).lower()]
314
+ return out
315
+
316
+ def wait_for_pull_requests(
317
+ self,
318
+ campaign_id: str,
319
+ min_count: int = 1,
320
+ poll_s: float = 5.0,
321
+ timeout_s: float = 5 * 60,
322
+ sleep: Optional[Callable[[float], None]] = None,
323
+ ) -> Dict[str, Any]:
324
+ _sleep = sleep or time.sleep
325
+ start = time.monotonic()
326
+ prs: List[Dict[str, Any]] = []
327
+ while time.monotonic() - start < timeout_s:
328
+ prs = self.list_pull_requests(campaign_id)
329
+ if len(prs) >= min_count:
330
+ return {"pull_requests": prs, "timed_out": False}
331
+ _sleep(poll_s)
332
+ prs = self.list_pull_requests(campaign_id)
333
+ return {"pull_requests": prs, "timed_out": len(prs) < min_count}
334
+
335
+ @staticmethod
336
+ def validate_remediation(remediate: bool, credential_id: Optional[str]) -> None:
337
+ """Guard for remediation (Pro) parameters.
338
+
339
+ When remediation is enabled a credential reference (an opaque vault id,
340
+ NOT a token) is required, validated before any request so a run is never
341
+ started half-configured.
342
+ """
343
+ if remediate and not str(credential_id or "").strip():
344
+ raise DarkmoonError(
345
+ "Remediation is enabled but no credential reference was provided. "
346
+ "Set the opaque Darkmoon credential reference (a vault id, not a token) "
347
+ "so the remediation agent can prepare a fix pull request for review."
348
+ )
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from lfx.io import MessageTextInput, Output
4
+ from lfx.schema.data import Data
5
+
6
+ from lfx.custom.custom_component.component import Component
7
+
8
+ from lfx_darkmoon.components.darkmoon._base import DarkmoonClientMixin, connection_inputs
9
+
10
+
11
+ class DarkmoonGetFindingsComponent(DarkmoonClientMixin, Component):
12
+ """Return the vulnerabilities and aggregated stats for a Darkmoon campaign."""
13
+
14
+ display_name = "Darkmoon Get Findings"
15
+ description = "Return the vulnerabilities and aggregated stats for a Darkmoon campaign id."
16
+ icon = "Shield"
17
+ name = "DarkmoonGetFindings"
18
+
19
+ inputs = [
20
+ *connection_inputs(),
21
+ MessageTextInput(
22
+ name="campaign_id",
23
+ display_name="Campaign id",
24
+ info="The Darkmoon campaign id, e.g. camp_20260922_abc123.",
25
+ required=True,
26
+ ),
27
+ ]
28
+
29
+ outputs = [Output(display_name="Findings", name="findings", method="get_findings_action")]
30
+
31
+ def get_findings_action(self) -> Data:
32
+ campaign_id = str(self.campaign_id or "").strip()
33
+ if not campaign_id:
34
+ raise ValueError("A campaign id is required.")
35
+ findings = self._client().get_findings(campaign_id)
36
+ data = Data(
37
+ data={
38
+ "campaign_id": campaign_id,
39
+ "total": findings["total"],
40
+ "stats": findings["stats"],
41
+ "findings": findings["data"],
42
+ }
43
+ )
44
+ self.status = data
45
+ return data
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from lfx.io import Output
4
+ from lfx.schema.data import Data
5
+
6
+ from lfx.custom.custom_component.component import Component
7
+
8
+ from lfx_darkmoon.components.darkmoon._base import DarkmoonClientMixin, connection_inputs
9
+
10
+
11
+ class DarkmoonListCampaignsComponent(DarkmoonClientMixin, Component):
12
+ """Return the campaigns visible to the authenticated dashboard user."""
13
+
14
+ display_name = "Darkmoon List Campaigns"
15
+ description = "List the Darkmoon campaigns visible to the authenticated dashboard user."
16
+ icon = "Shield"
17
+ name = "DarkmoonListCampaigns"
18
+
19
+ inputs = [*connection_inputs()]
20
+
21
+ outputs = [Output(display_name="Campaigns", name="campaigns", method="list_campaigns_action")]
22
+
23
+ def list_campaigns_action(self) -> Data:
24
+ campaigns = self._client().list_campaigns()
25
+ data = Data(data={"total": len(campaigns), "campaigns": campaigns})
26
+ self.status = data
27
+ return data
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from lfx.io import DropdownInput, MessageTextInput, Output
4
+ from lfx.schema.data import Data
5
+
6
+ from lfx.custom.custom_component.component import Component
7
+
8
+ from lfx_darkmoon.components.darkmoon._base import DarkmoonClientMixin, connection_inputs
9
+ from lfx_darkmoon.components.darkmoon.darkmoon_client import DarkmoonClient, PR_STATES
10
+
11
+
12
+ class DarkmoonListPullRequestsComponent(DarkmoonClientMixin, Component):
13
+ """Return the fix pull requests Darkmoon prepared (read-only).
14
+
15
+ Pull requests are prepared by the paid Pro remediation feature and left for a
16
+ human to review and merge. This component only reads them; it never merges.
17
+ """
18
+
19
+ display_name = "Darkmoon List Pull Requests"
20
+ description = "List the fix pull requests Darkmoon prepared (read-only)."
21
+ icon = "Shield"
22
+ name = "DarkmoonListPullRequests"
23
+
24
+ inputs = [
25
+ *connection_inputs(),
26
+ MessageTextInput(
27
+ name="campaign_id",
28
+ display_name="Campaign id",
29
+ info="Optional campaign id to scope the pull requests to (the only server-side filter).",
30
+ advanced=True,
31
+ ),
32
+ DropdownInput(
33
+ name="state",
34
+ display_name="State",
35
+ options=["", *PR_STATES],
36
+ value="",
37
+ advanced=True,
38
+ ),
39
+ ]
40
+
41
+ outputs = [Output(display_name="Pull Requests", name="pull_requests", method="list_pull_requests_action")]
42
+
43
+ def list_pull_requests_action(self) -> Data:
44
+ campaign_id = str(self.campaign_id or "").strip() or None
45
+ client = self._client()
46
+ prs = client.list_pull_requests(campaign_id)
47
+ state = str(self.state or "").strip()
48
+ if state:
49
+ prs = DarkmoonClient.filter_pull_requests(prs, state=[state])
50
+ data = Data(data={"total": len(prs), "pull_requests": prs})
51
+ self.status = data
52
+ return data
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from lfx.io import BoolInput, DropdownInput, MessageTextInput, Output, SecretStrInput, StrInput
4
+ from lfx.schema.data import Data
5
+
6
+ from lfx.custom.custom_component.component import Component
7
+
8
+ from lfx_darkmoon.components.darkmoon._base import DarkmoonClientMixin, connection_inputs
9
+ from lfx_darkmoon.components.darkmoon.darkmoon_client import DarkmoonClient
10
+
11
+
12
+ def _split_csv(value: str) -> list[str]:
13
+ return [p.strip() for p in str(value or "").split(",") if p.strip()]
14
+
15
+
16
+ class DarkmoonRunPentestComponent(DarkmoonClientMixin, Component):
17
+ """Trigger a Darkmoon pentest against an authorised target and return findings.
18
+
19
+ Only run assessments against systems you own or have explicit written
20
+ authorisation to test. Findings can include false positives and must be
21
+ reviewed by a qualified human. This component never merges a pull request;
22
+ optional, credential-gated remediation is a paid Pro feature that only
23
+ prepares fixes for review.
24
+ """
25
+
26
+ display_name = "Darkmoon Run Pentest"
27
+ description = "Start a Darkmoon AI pentest against an authorised target and return the findings."
28
+ icon = "Shield"
29
+ name = "DarkmoonRunPentest"
30
+
31
+ inputs = [
32
+ *connection_inputs(),
33
+ MessageTextInput(
34
+ name="target",
35
+ display_name="Target",
36
+ info="The host, URL or scope to assess. Only targets you are authorised to test.",
37
+ required=True,
38
+ ),
39
+ BoolInput(
40
+ name="wait_for_completion",
41
+ display_name="Wait for completion",
42
+ info="Wait for the run to finish and return findings; if off, return the run id.",
43
+ value=True,
44
+ ),
45
+ MessageTextInput(name="program", display_name="Program / rules of engagement", advanced=True),
46
+ MessageTextInput(
47
+ name="focus",
48
+ display_name="Focus areas",
49
+ info='Optional comma-separated focus areas, e.g. "authentication, injection".',
50
+ advanced=True,
51
+ ),
52
+ DropdownInput(
53
+ name="severity",
54
+ display_name="Minimum severity",
55
+ options=["", "info", "low", "medium", "high", "critical"],
56
+ value="",
57
+ advanced=True,
58
+ ),
59
+ BoolInput(
60
+ name="remediate",
61
+ display_name="Prepare fix pull requests (Pro)",
62
+ info="Paid Pro feature. Prepares fix pull requests for human review during the run; never merges. Requires a credential reference.",
63
+ value=False,
64
+ advanced=True,
65
+ ),
66
+ SecretStrInput(
67
+ name="credential_id",
68
+ display_name="Credential reference (Pro remediation)",
69
+ info="Opaque Darkmoon vault credential reference (not a token). Required only when remediation is on.",
70
+ advanced=True,
71
+ ),
72
+ StrInput(name="git_repo", display_name="Git repository (Pro remediation)", advanced=True),
73
+ ]
74
+
75
+ outputs = [Output(display_name="Result", name="result", method="run_pentest_action")]
76
+
77
+ def run_pentest_action(self) -> Data:
78
+ target = str(self.target or "").strip()
79
+ if not target:
80
+ raise ValueError("A target is required (a host, URL or scope you are authorised to test).")
81
+ remediate = bool(self.remediate)
82
+ credential_id = str(self.credential_id or "").strip() or None
83
+ DarkmoonClient.validate_remediation(remediate, credential_id)
84
+
85
+ params: dict = {"target": target}
86
+ if str(self.program or "").strip():
87
+ params["program"] = str(self.program).strip()
88
+ areas = _split_csv(self.focus)
89
+ if areas:
90
+ params["focus"] = areas
91
+ if str(self.severity or "").strip():
92
+ params["severity"] = str(self.severity).strip()
93
+ if remediate:
94
+ params["remediate"] = True
95
+ params["credential_id"] = credential_id
96
+ if str(self.git_repo or "").strip():
97
+ params["git_repo"] = str(self.git_repo).strip()
98
+
99
+ client = self._client()
100
+ before = {c.get("id") for c in client.list_campaigns()}
101
+ handle = client.run_campaign(params)
102
+ run_id = handle.get("run_id")
103
+
104
+ if not bool(self.wait_for_completion):
105
+ data = Data(data={"status": "started", "run_id": run_id, "target": target})
106
+ self.status = data
107
+ return data
108
+
109
+ result = client.wait_for_run(run_id)
110
+ campaign = client.resolve_run_campaign(before, target)
111
+ campaign_id = campaign.get("id") if campaign else None
112
+ out = {
113
+ "run_id": run_id,
114
+ "campaign_id": campaign_id,
115
+ "timed_out": result["timed_out"],
116
+ "findings": [],
117
+ "stats": {},
118
+ "total": 0,
119
+ }
120
+ if campaign_id:
121
+ findings = client.get_findings(campaign_id)
122
+ out["findings"] = findings["data"]
123
+ out["stats"] = findings["stats"]
124
+ out["total"] = findings["total"]
125
+ data = Data(data=out)
126
+ self.status = data
127
+ return data
@@ -0,0 +1,46 @@
1
+ """
2
+ requests-based transport + client factory for the Langflow Darkmoon components.
3
+
4
+ Kept out of ``darkmoon_client`` so the client stays dependency free and
5
+ unit-testable without ``requests``. The transport never raises on a non-2xx
6
+ status: it returns the real status and parsed body so the client surfaces the
7
+ API's own ``detail`` rather than a local secret.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Dict
12
+
13
+ from lfx_darkmoon.components.darkmoon.darkmoon_client import DarkmoonClient, HttpResponse
14
+
15
+ DEFAULT_TIMEOUT = 60
16
+
17
+
18
+ def requests_transport(opts: Dict[str, Any]) -> HttpResponse:
19
+ import requests # imported lazily so tests need no network stack
20
+
21
+ method = str(opts.get("method", "GET")).upper()
22
+ url = opts["url"]
23
+ headers = opts.get("headers") or {}
24
+ body = opts.get("body")
25
+ try:
26
+ resp = requests.request(
27
+ method,
28
+ url,
29
+ headers=headers,
30
+ json=body if body is not None else None,
31
+ timeout=DEFAULT_TIMEOUT,
32
+ )
33
+ except requests.exceptions.RequestException as exc:
34
+ return HttpResponse(status_code=0, body={"detail": f"Request to Darkmoon failed: {exc}"})
35
+ try:
36
+ parsed: Any = resp.json()
37
+ except ValueError:
38
+ parsed = resp.text
39
+ return HttpResponse(status_code=resp.status_code, body=parsed)
40
+
41
+
42
+ def logged_in_client(base_url: str, username: str, password: str) -> DarkmoonClient:
43
+ """Build a DarkmoonClient over the requests transport and authenticate it."""
44
+ client = DarkmoonClient(base_url, requests_transport)
45
+ client.login(username, password)
46
+ return client
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://schemas.langflow.org/extension/v1.json",
3
+ "id": "lfx-darkmoon",
4
+ "version": "0.1.0",
5
+ "name": "Darkmoon",
6
+ "description": "Trigger a Darkmoon autonomous AI penetration test against an authorised target, pull findings, and review the fix pull requests Darkmoon prepares, over a self-hosted Darkmoon Dashboard API. Only assess systems you own or have explicit written authorisation to test; findings can include false positives and must be reviewed by a qualified human.",
7
+ "lfx": {
8
+ "compat": ["1"]
9
+ },
10
+ "capabilities": {
11
+ "requiresCredentials": true
12
+ },
13
+ "bundles": [
14
+ {
15
+ "name": "darkmoon",
16
+ "path": "components/darkmoon",
17
+ "display_name": "Darkmoon",
18
+ "icon": "Shield"
19
+ }
20
+ ]
21
+ }