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.
@@ -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
+ }