aisoc-cli 0.1.0__py3-none-any.whl
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.
- aisoc_cli/__init__.py +3 -0
- aisoc_cli/main.py +1372 -0
- aisoc_cli/templates/connector/README.md.tmpl +37 -0
- aisoc_cli/templates/connector/connector.py.tmpl +106 -0
- aisoc_cli/templates/connector/plugin.yaml.tmpl +42 -0
- aisoc_cli/templates/detection/README.md.tmpl +30 -0
- aisoc_cli/templates/detection/plugin.yaml.tmpl +17 -0
- aisoc_cli/templates/detection/rules/example.yaml.tmpl +28 -0
- aisoc_cli/templates/enricher/README.md.tmpl +29 -0
- aisoc_cli/templates/enricher/plugin.py.tmpl +42 -0
- aisoc_cli/templates/enricher/plugin.yaml.tmpl +29 -0
- aisoc_cli/templates/responder/README.md.tmpl +25 -0
- aisoc_cli/templates/responder/plugin.py.tmpl +66 -0
- aisoc_cli/templates/responder/plugin.yaml.tmpl +32 -0
- aisoc_cli/templates/widget/README.md.tmpl +40 -0
- aisoc_cli/templates/widget/plugin.yaml.tmpl +29 -0
- aisoc_cli/templates/widget/widget.py.tmpl +50 -0
- aisoc_cli-0.1.0.dist-info/METADATA +90 -0
- aisoc_cli-0.1.0.dist-info/RECORD +21 -0
- aisoc_cli-0.1.0.dist-info/WHEEL +4 -0
- aisoc_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
AiSOC **connector** plugin generated by `aisoc plugin new`.
|
|
4
|
+
|
|
5
|
+
## Where this lives
|
|
6
|
+
|
|
7
|
+
Connectors are run by `services/connectors`. To wire this up locally, drop the
|
|
8
|
+
class into `services/connectors/app/connectors/$slug.py` and add it to the
|
|
9
|
+
`_CONNECTOR_CLASSES` registry in `services/connectors/app/connectors/__init__.py`
|
|
10
|
+
(see `AGENTS.md` → "Connector platform conventions").
|
|
11
|
+
|
|
12
|
+
## Severity ladder
|
|
13
|
+
|
|
14
|
+
The AiSOC severity ladder is exactly four tiers:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
info | low | medium | high
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Every vendor-native ladder must collapse into this set inside `normalize()`.
|
|
21
|
+
Fill out the `SEVERITY_MAP` dict in `connector.py`.
|
|
22
|
+
|
|
23
|
+
## Develop
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
aisoc plugin validate plugin.yaml
|
|
27
|
+
pytest -v # add tests under tests/test_${slug}_connector.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Publish
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
aisoc plugin publish .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See [apps/docs/docs/connectors](https://github.com/beenuar/AiSOC/tree/main/apps/docs/docs/connectors)
|
|
37
|
+
for the per-connector setup walkthrough format.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
$name — AiSOC connector plugin.
|
|
3
|
+
|
|
4
|
+
Connectors are owned by ``services/connectors``. They subclass ``BaseConnector``
|
|
5
|
+
and implement three async methods:
|
|
6
|
+
|
|
7
|
+
- ``test_connection()`` — quick health check used by the UI "Test" button.
|
|
8
|
+
- ``fetch_alerts()`` — pull a batch of upstream records on each poll tick.
|
|
9
|
+
- ``normalize()`` — map vendor records onto the AiSOC OCSF-aligned schema.
|
|
10
|
+
|
|
11
|
+
The in-process ``ConnectorScheduler`` will instantiate this class once per
|
|
12
|
+
enabled tenant instance and call ``fetch_alerts`` on the configured cadence.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
# When this plugin lives inside the AiSOC monorepo, prefer the real base class:
|
|
21
|
+
# from services.connectors.app.connectors.base import BaseConnector, ConnectorSchema
|
|
22
|
+
# The shim below lets the scaffold lint cleanly outside the monorepo.
|
|
23
|
+
try: # pragma: no cover
|
|
24
|
+
from services.connectors.app.connectors.base import BaseConnector, ConnectorSchema
|
|
25
|
+
except Exception: # pragma: no cover
|
|
26
|
+
BaseConnector = object # type: ignore[misc,assignment]
|
|
27
|
+
ConnectorSchema = dict # type: ignore[misc,assignment]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
SEVERITY_MAP: dict[str, str] = {
|
|
31
|
+
# Map vendor-native severities onto AiSOC's four-tier ladder.
|
|
32
|
+
# info | low | medium | high
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Connector(BaseConnector): # type: ignore[misc]
|
|
37
|
+
"""$name connector."""
|
|
38
|
+
|
|
39
|
+
connector_id = "$slug"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def schema(cls) -> ConnectorSchema: # type: ignore[override]
|
|
43
|
+
# Mirrors plugin.yaml.config_schema. Kept as a classmethod so the API
|
|
44
|
+
# service can describe this connector without instantiating it.
|
|
45
|
+
return ConnectorSchema( # type: ignore[call-arg]
|
|
46
|
+
name="$slug",
|
|
47
|
+
label="$name",
|
|
48
|
+
description="$name connector",
|
|
49
|
+
category="siem", # edr | siem | cloud | iam | saas | vcs | network
|
|
50
|
+
fields=[],
|
|
51
|
+
oauth=None,
|
|
52
|
+
default_poll_interval_seconds=300,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __init__(self, instance_id: str, tenant_id: str, config: dict[str, Any]) -> None:
|
|
56
|
+
super().__init__(instance_id=instance_id, tenant_id=tenant_id, config=config)
|
|
57
|
+
self.base_url: str = config["base_url"].rstrip("/")
|
|
58
|
+
self.api_key: str | None = config.get("api_key")
|
|
59
|
+
self.verify_tls: bool = bool(config.get("verify_tls", True))
|
|
60
|
+
self.page_size: int = int(config.get("page_size", 100))
|
|
61
|
+
|
|
62
|
+
def _client(self) -> httpx.AsyncClient:
|
|
63
|
+
headers = {"Accept": "application/json"}
|
|
64
|
+
if self.api_key:
|
|
65
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
66
|
+
return httpx.AsyncClient(
|
|
67
|
+
base_url=self.base_url,
|
|
68
|
+
headers=headers,
|
|
69
|
+
verify=self.verify_tls,
|
|
70
|
+
timeout=httpx.Timeout(15.0, connect=5.0),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def test_connection(self) -> dict[str, Any]:
|
|
74
|
+
"""Quick reachability + auth check."""
|
|
75
|
+
async with self._client() as client:
|
|
76
|
+
resp = await client.get("/health")
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
return {"ok": True, "status": resp.status_code}
|
|
79
|
+
|
|
80
|
+
async def fetch_alerts(self, since_iso: str | None = None) -> list[dict[str, Any]]:
|
|
81
|
+
"""Pull a batch of vendor records.
|
|
82
|
+
|
|
83
|
+
The scheduler hands us the high-watermark from the last successful tick
|
|
84
|
+
as ``since_iso``. Use it to ask the upstream for "everything newer than".
|
|
85
|
+
"""
|
|
86
|
+
params: dict[str, Any] = {"limit": self.page_size}
|
|
87
|
+
if since_iso:
|
|
88
|
+
params["since"] = since_iso
|
|
89
|
+
|
|
90
|
+
async with self._client() as client:
|
|
91
|
+
resp = await client.get("/alerts", params=params)
|
|
92
|
+
resp.raise_for_status()
|
|
93
|
+
data = resp.json()
|
|
94
|
+
return list(data.get("results", []))
|
|
95
|
+
|
|
96
|
+
def normalize(self, raw: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
"""Map a vendor record onto the AiSOC OCSF-aligned event shape."""
|
|
98
|
+
return {
|
|
99
|
+
"source": "$slug",
|
|
100
|
+
"vendor_id": str(raw.get("id", "")),
|
|
101
|
+
"timestamp": raw.get("timestamp"),
|
|
102
|
+
"title": raw.get("title", ""),
|
|
103
|
+
"description": raw.get("description", ""),
|
|
104
|
+
"severity": SEVERITY_MAP.get(str(raw.get("severity", "")).lower(), "info"),
|
|
105
|
+
"raw": raw,
|
|
106
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
id: $slug
|
|
2
|
+
name: $name
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
plugin_type: connector
|
|
5
|
+
tier: community
|
|
6
|
+
description: >
|
|
7
|
+
$name pulls alerts/events from an upstream source and normalizes them
|
|
8
|
+
onto the AiSOC OCSF-aligned schema with the four-tier severity ladder
|
|
9
|
+
(info | low | medium | high).
|
|
10
|
+
author: "$author"
|
|
11
|
+
tags: [connector]
|
|
12
|
+
license: MIT
|
|
13
|
+
min_aisoc_version: "4.0.0"
|
|
14
|
+
runtime: python
|
|
15
|
+
entry_point: connector.py
|
|
16
|
+
config_schema:
|
|
17
|
+
type: object
|
|
18
|
+
required: [base_url]
|
|
19
|
+
properties:
|
|
20
|
+
base_url:
|
|
21
|
+
type: string
|
|
22
|
+
description: "Upstream API base URL, e.g. https://api.example.com"
|
|
23
|
+
api_key:
|
|
24
|
+
type: string
|
|
25
|
+
description: "API key / bearer token. Stored encrypted in the credential vault."
|
|
26
|
+
secret: true
|
|
27
|
+
poll_interval_seconds:
|
|
28
|
+
type: integer
|
|
29
|
+
description: "How often the in-process scheduler should poll. Default 300s."
|
|
30
|
+
default: 300
|
|
31
|
+
minimum: 30
|
|
32
|
+
maximum: 3600
|
|
33
|
+
verify_tls:
|
|
34
|
+
type: boolean
|
|
35
|
+
description: "Verify TLS certs on the upstream. Disable only for dev."
|
|
36
|
+
default: true
|
|
37
|
+
page_size:
|
|
38
|
+
type: integer
|
|
39
|
+
description: "Records per upstream API page."
|
|
40
|
+
default: 100
|
|
41
|
+
minimum: 1
|
|
42
|
+
maximum: 10000
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
AiSOC **detection** plugin generated by `aisoc plugin new`.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
$slug/
|
|
9
|
+
├── plugin.yaml
|
|
10
|
+
├── README.md
|
|
11
|
+
└── rules/
|
|
12
|
+
└── example.yaml # add one YAML file per rule
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Detection rules use the AiSOC schema (severity ladder is exactly
|
|
16
|
+
`info | low | medium | high`). See `detections/` in the AiSOC monorepo for
|
|
17
|
+
working examples across endpoint, network, cloud, identity, and application.
|
|
18
|
+
|
|
19
|
+
## Develop
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
aisoc plugin validate plugin.yaml
|
|
23
|
+
aisoc detection validate rules/example.yaml
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Publish
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
aisoc plugin publish .
|
|
30
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
id: $slug
|
|
2
|
+
name: $name
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
plugin_type: detection
|
|
5
|
+
tier: community
|
|
6
|
+
description: >
|
|
7
|
+
$name ships one or more AiSOC detection rules. Each rule lives in
|
|
8
|
+
``rules/*.yaml`` and is validated by ``aisoc detection validate``.
|
|
9
|
+
author: "$author"
|
|
10
|
+
tags: [detection]
|
|
11
|
+
license: MIT
|
|
12
|
+
min_aisoc_version: "4.0.0"
|
|
13
|
+
runtime: python
|
|
14
|
+
entry_point: rules/
|
|
15
|
+
config_schema:
|
|
16
|
+
type: object
|
|
17
|
+
properties: {}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id: det-$slug-001
|
|
2
|
+
name: Example detection from $name
|
|
3
|
+
description: >
|
|
4
|
+
Replace this with a one-paragraph description of what this rule fires on,
|
|
5
|
+
why it matters, and which adversary behaviour it covers.
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
severity: medium # info | low | medium | high
|
|
8
|
+
tags:
|
|
9
|
+
- mitre.attack.t0000
|
|
10
|
+
- tlp.white
|
|
11
|
+
category: endpoint # endpoint | network | cloud | identity | application
|
|
12
|
+
log_source:
|
|
13
|
+
product: example
|
|
14
|
+
service: example
|
|
15
|
+
detection:
|
|
16
|
+
condition: |
|
|
17
|
+
# TODO: replace with a real boolean expression over normalized event fields.
|
|
18
|
+
event.action == "example_action"
|
|
19
|
+
AND event.outcome == "success"
|
|
20
|
+
false_positives:
|
|
21
|
+
- List the realistic noise sources here so analysts know how to tune.
|
|
22
|
+
playbook: tpl-investigation
|
|
23
|
+
enabled: true
|
|
24
|
+
author: "$author"
|
|
25
|
+
references:
|
|
26
|
+
- https://attack.mitre.org/
|
|
27
|
+
created: '2026-01-01'
|
|
28
|
+
modified: '2026-01-01'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
AiSOC **enricher** plugin generated by `aisoc plugin new`.
|
|
4
|
+
|
|
5
|
+
Enrichers add context to in-flight events. They are called by the orchestrator
|
|
6
|
+
once per matching event and must:
|
|
7
|
+
|
|
8
|
+
- Be fast (sub-second). Set `timeout_seconds` low.
|
|
9
|
+
- Be side-effect-free.
|
|
10
|
+
- Never raise on a miss — return `{}` instead.
|
|
11
|
+
|
|
12
|
+
## Develop
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Validate the manifest
|
|
16
|
+
aisoc plugin validate plugin.yaml
|
|
17
|
+
|
|
18
|
+
# Run unit tests once you add them
|
|
19
|
+
pytest -v
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Publish
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
aisoc plugin publish .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See the [AiSOC plugin docs](https://github.com/beenuar/AiSOC/tree/main/apps/docs/docs/plugins)
|
|
29
|
+
for the full lifecycle.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
$name — AiSOC enricher plugin.
|
|
3
|
+
|
|
4
|
+
Enrichers receive a normalized event payload and return a small dict of
|
|
5
|
+
additional context to merge back into the event. They run on the hot path
|
|
6
|
+
inside the orchestrator graph, so keep them fast and side-effect-free.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Plugin:
|
|
14
|
+
"""Main plugin class — AiSOC instantiates this once per tenant per worker
|
|
15
|
+
and calls ``run()`` once per matching event.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
19
|
+
self.config = config
|
|
20
|
+
self.api_key: str | None = config.get("api_key")
|
|
21
|
+
self.timeout_seconds: int = int(config.get("timeout_seconds", 5))
|
|
22
|
+
|
|
23
|
+
async def run(
|
|
24
|
+
self,
|
|
25
|
+
payload: dict[str, Any],
|
|
26
|
+
context: dict[str, Any],
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Enrich the alert/event payload.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
payload: Normalized event in OCSF-aligned shape.
|
|
32
|
+
context: Runtime context — ``tenant_id``, ``trace_id``, etc.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict to merge into ``payload["enrichments"]["$slug"]``. Return an
|
|
36
|
+
empty dict if you have nothing to add — never raise on misses.
|
|
37
|
+
"""
|
|
38
|
+
# TODO: replace with your enrichment lookup.
|
|
39
|
+
return {
|
|
40
|
+
"enriched_by": "$slug",
|
|
41
|
+
"data": {},
|
|
42
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
id: $slug
|
|
2
|
+
name: $name
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
plugin_type: enricher
|
|
5
|
+
tier: community
|
|
6
|
+
description: >
|
|
7
|
+
$name enriches alerts and events with additional context — IoC reputation,
|
|
8
|
+
asset metadata, user attributes, geolocation, etc. AiSOC calls run() once
|
|
9
|
+
per matching event in the orchestrator pipeline.
|
|
10
|
+
author: "$author"
|
|
11
|
+
tags: [enricher]
|
|
12
|
+
license: MIT
|
|
13
|
+
min_aisoc_version: "4.0.0"
|
|
14
|
+
runtime: python
|
|
15
|
+
entry_point: plugin.py
|
|
16
|
+
config_schema:
|
|
17
|
+
type: object
|
|
18
|
+
required: []
|
|
19
|
+
properties:
|
|
20
|
+
api_key:
|
|
21
|
+
type: string
|
|
22
|
+
description: "API key for the upstream enrichment service."
|
|
23
|
+
secret: true
|
|
24
|
+
timeout_seconds:
|
|
25
|
+
type: integer
|
|
26
|
+
description: "Per-request timeout. Keep low — enrichers run on the hot path."
|
|
27
|
+
default: 5
|
|
28
|
+
minimum: 1
|
|
29
|
+
maximum: 30
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
AiSOC **responder** plugin generated by `aisoc plugin new`.
|
|
4
|
+
|
|
5
|
+
## Contract
|
|
6
|
+
|
|
7
|
+
- `supported_actions` — list of verbs your responder understands.
|
|
8
|
+
- `execute(action, params, context)` — returns
|
|
9
|
+
`{"status": "ok" | "skipped" | "error", "detail": ..., "artifact": ...}`.
|
|
10
|
+
|
|
11
|
+
Always honour `dry_run=true` and return `status="skipped"`. The investigation
|
|
12
|
+
timeline relies on this to render an honest record of what was actually done.
|
|
13
|
+
|
|
14
|
+
## Develop
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
aisoc plugin validate plugin.yaml
|
|
18
|
+
pytest -v
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Publish
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
aisoc plugin publish .
|
|
25
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
$name — AiSOC responder plugin.
|
|
3
|
+
|
|
4
|
+
Responders execute containment / remediation actions. A playbook step calls
|
|
5
|
+
``Plugin.execute(action, params, context)`` and expects a structured result
|
|
6
|
+
back so the timeline can render what happened.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Plugin:
|
|
14
|
+
"""Main plugin class. AiSOC instantiates one per tenant per worker."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
17
|
+
self.config = config
|
|
18
|
+
self.api_key: str | None = config.get("api_key")
|
|
19
|
+
self.dry_run: bool = bool(config.get("dry_run", False))
|
|
20
|
+
self.timeout_seconds: int = int(config.get("timeout_seconds", 15))
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def supported_actions(self) -> list[str]:
|
|
24
|
+
"""Action names this responder knows how to run."""
|
|
25
|
+
# TODO: list the verbs your responder supports, e.g. ["disable_user",
|
|
26
|
+
# "isolate_host", "block_ip"]. The orchestrator validates against this.
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
async def execute(
|
|
30
|
+
self,
|
|
31
|
+
action: str,
|
|
32
|
+
params: dict[str, Any],
|
|
33
|
+
context: dict[str, Any],
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Run a single action.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
action: Verb chosen by the playbook (one of ``supported_actions``).
|
|
39
|
+
params: Action-specific arguments (target user, host, IP, etc.).
|
|
40
|
+
context: Runtime context — ``tenant_id``, ``trace_id``, ``case_id``.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
``{"status": "ok" | "skipped" | "error", "detail": ..., "artifact": ...}``
|
|
44
|
+
|
|
45
|
+
``status="skipped"`` should be used in dry-run mode so the timeline
|
|
46
|
+
stays honest about what really happened.
|
|
47
|
+
"""
|
|
48
|
+
if action not in self.supported_actions:
|
|
49
|
+
return {
|
|
50
|
+
"status": "error",
|
|
51
|
+
"detail": f"Unsupported action: {action}",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if self.dry_run:
|
|
55
|
+
return {
|
|
56
|
+
"status": "skipped",
|
|
57
|
+
"detail": "dry_run=true",
|
|
58
|
+
"artifact": {"action": action, "params": params},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# TODO: implement the real call to the upstream control plane.
|
|
62
|
+
return {
|
|
63
|
+
"status": "ok",
|
|
64
|
+
"detail": f"$slug executed {action}",
|
|
65
|
+
"artifact": {"action": action, "params": params},
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
id: $slug
|
|
2
|
+
name: $name
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
plugin_type: responder
|
|
5
|
+
tier: community
|
|
6
|
+
description: >
|
|
7
|
+
$name takes a containment / remediation action when fired by a playbook.
|
|
8
|
+
Examples: disable a user, isolate an endpoint, block an IP at the edge.
|
|
9
|
+
author: "$author"
|
|
10
|
+
tags: [responder]
|
|
11
|
+
license: MIT
|
|
12
|
+
min_aisoc_version: "4.0.0"
|
|
13
|
+
runtime: python
|
|
14
|
+
entry_point: plugin.py
|
|
15
|
+
config_schema:
|
|
16
|
+
type: object
|
|
17
|
+
required: []
|
|
18
|
+
properties:
|
|
19
|
+
api_key:
|
|
20
|
+
type: string
|
|
21
|
+
description: "API token used to call the upstream control plane."
|
|
22
|
+
secret: true
|
|
23
|
+
dry_run:
|
|
24
|
+
type: boolean
|
|
25
|
+
description: "If true, log the intended action without executing it."
|
|
26
|
+
default: false
|
|
27
|
+
timeout_seconds:
|
|
28
|
+
type: integer
|
|
29
|
+
description: "Per-action timeout."
|
|
30
|
+
default: 15
|
|
31
|
+
minimum: 1
|
|
32
|
+
maximum: 120
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
AiSOC **widget** plugin generated by `aisoc plugin new`.
|
|
4
|
+
|
|
5
|
+
## Contract
|
|
6
|
+
|
|
7
|
+
`Widget.fetch_data(context)` returns a dict shaped like:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"type": "kpi",
|
|
12
|
+
"title": "string",
|
|
13
|
+
"data": { "value": 0, "label": "events" },
|
|
14
|
+
"refresh_interval_seconds": 60
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`type` is one of:
|
|
19
|
+
|
|
20
|
+
| type | `data` shape | renders as |
|
|
21
|
+
| ------------ | --------------------------------------------- | ----------------------- |
|
|
22
|
+
| `kpi` | `{"value": number, "label": str}` | big-number card |
|
|
23
|
+
| `timeseries` | `[{"t": iso8601, "v": number}, ...]` | line chart |
|
|
24
|
+
| `table` | `{"columns": [str], "rows": [[Any]]}` | dense table |
|
|
25
|
+
| `list` | `[{"primary": str, "secondary": str}, ...]` | top-N list |
|
|
26
|
+
|
|
27
|
+
Keep `fetch_data()` fast — widgets are polled on every dashboard tick.
|
|
28
|
+
|
|
29
|
+
## Develop
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
aisoc plugin validate plugin.yaml
|
|
33
|
+
pytest -v
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Publish
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
aisoc plugin publish .
|
|
40
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
id: $slug
|
|
2
|
+
name: $name
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
plugin_type: widget
|
|
5
|
+
tier: community
|
|
6
|
+
description: >
|
|
7
|
+
$name is a custom dashboard widget for AiSOC. The widget is rendered inside
|
|
8
|
+
the saved-views / dashboard surface and receives a small query API plus the
|
|
9
|
+
current tenant's theme tokens.
|
|
10
|
+
author: "$author"
|
|
11
|
+
tags: [widget]
|
|
12
|
+
license: MIT
|
|
13
|
+
min_aisoc_version: "4.0.0"
|
|
14
|
+
runtime: python
|
|
15
|
+
entry_point: widget.py
|
|
16
|
+
config_schema:
|
|
17
|
+
type: object
|
|
18
|
+
required: []
|
|
19
|
+
properties:
|
|
20
|
+
title:
|
|
21
|
+
type: string
|
|
22
|
+
description: "Display title shown in the widget header."
|
|
23
|
+
default: "$name"
|
|
24
|
+
refresh_interval_seconds:
|
|
25
|
+
type: integer
|
|
26
|
+
description: "How often the dashboard should re-fetch the widget's data."
|
|
27
|
+
default: 60
|
|
28
|
+
minimum: 5
|
|
29
|
+
maximum: 3600
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
$name — AiSOC dashboard widget plugin.
|
|
3
|
+
|
|
4
|
+
Widgets are rendered inside the AiSOC dashboard / saved-views surface. The
|
|
5
|
+
backend ``Widget.fetch_data()`` call returns a small typed payload that the
|
|
6
|
+
frontend renderer turns into a card, chart, or table.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Widget:
|
|
14
|
+
"""Main widget class — AiSOC instantiates one per dashboard placement."""
|
|
15
|
+
|
|
16
|
+
widget_id = "$slug"
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
19
|
+
self.config = config
|
|
20
|
+
self.title: str = str(config.get("title", "$name"))
|
|
21
|
+
self.refresh_interval_seconds: int = int(
|
|
22
|
+
config.get("refresh_interval_seconds", 60)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def fetch_data(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
"""Return the data payload the dashboard should render.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
context: Runtime context — ``tenant_id``, ``user_id``, ``time_range``.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A dict with the shape::
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"type": "kpi" | "timeseries" | "table" | "list",
|
|
36
|
+
"title": str,
|
|
37
|
+
"data": Any, # type-specific payload
|
|
38
|
+
"refresh_interval_seconds": int,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
``data`` is widget-type-specific. KPI widgets return a single number,
|
|
42
|
+
timeseries return ``[{"t": iso8601, "v": number}, ...]``, etc.
|
|
43
|
+
"""
|
|
44
|
+
# TODO: query the AiSOC API or directly run the widget's data source.
|
|
45
|
+
return {
|
|
46
|
+
"type": "kpi",
|
|
47
|
+
"title": self.title,
|
|
48
|
+
"data": {"value": 0, "label": "events"},
|
|
49
|
+
"refresh_interval_seconds": self.refresh_interval_seconds,
|
|
50
|
+
}
|