qarbaal 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.
Files changed (39) hide show
  1. qarbaal-0.1.0/LICENSE +21 -0
  2. qarbaal-0.1.0/PKG-INFO +154 -0
  3. qarbaal-0.1.0/README.md +101 -0
  4. qarbaal-0.1.0/pyproject.toml +102 -0
  5. qarbaal-0.1.0/qarbaal/__init__.py +77 -0
  6. qarbaal-0.1.0/qarbaal/__main__.py +7 -0
  7. qarbaal-0.1.0/qarbaal/adapters.py +98 -0
  8. qarbaal-0.1.0/qarbaal/cli.py +254 -0
  9. qarbaal-0.1.0/qarbaal/data/tlds.txt +1437 -0
  10. qarbaal-0.1.0/qarbaal/enrich.py +182 -0
  11. qarbaal-0.1.0/qarbaal/exceptions.py +33 -0
  12. qarbaal-0.1.0/qarbaal/export.py +613 -0
  13. qarbaal-0.1.0/qarbaal/extract.py +1046 -0
  14. qarbaal-0.1.0/qarbaal/ingest.py +432 -0
  15. qarbaal-0.1.0/qarbaal/normalize.py +171 -0
  16. qarbaal-0.1.0/qarbaal/patterns.py +129 -0
  17. qarbaal-0.1.0/qarbaal/pivots.py +91 -0
  18. qarbaal-0.1.0/qarbaal/py.typed +0 -0
  19. qarbaal-0.1.0/qarbaal/refang.py +132 -0
  20. qarbaal-0.1.0/qarbaal/sample.py +36 -0
  21. qarbaal-0.1.0/qarbaal/tlds.py +139 -0
  22. qarbaal-0.1.0/qarbaal/types.py +367 -0
  23. qarbaal-0.1.0/qarbaal/validate.py +263 -0
  24. qarbaal-0.1.0/qarbaal/version.py +6 -0
  25. qarbaal-0.1.0/qarbaal.egg-info/PKG-INFO +154 -0
  26. qarbaal-0.1.0/qarbaal.egg-info/SOURCES.txt +37 -0
  27. qarbaal-0.1.0/qarbaal.egg-info/dependency_links.txt +1 -0
  28. qarbaal-0.1.0/qarbaal.egg-info/entry_points.txt +2 -0
  29. qarbaal-0.1.0/qarbaal.egg-info/requires.txt +35 -0
  30. qarbaal-0.1.0/qarbaal.egg-info/top_level.txt +1 -0
  31. qarbaal-0.1.0/setup.cfg +4 -0
  32. qarbaal-0.1.0/tests/test_enrich_provenance.py +84 -0
  33. qarbaal-0.1.0/tests/test_export.py +119 -0
  34. qarbaal-0.1.0/tests/test_extra_types.py +98 -0
  35. qarbaal-0.1.0/tests/test_extract.py +157 -0
  36. qarbaal-0.1.0/tests/test_ingest_cli.py +172 -0
  37. qarbaal-0.1.0/tests/test_normalize.py +49 -0
  38. qarbaal-0.1.0/tests/test_refang.py +38 -0
  39. qarbaal-0.1.0/tests/test_validate.py +73 -0
qarbaal-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ahur4
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.
qarbaal-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: qarbaal
3
+ Version: 0.1.0
4
+ Summary: Extract, normalize, validate, and export indicators of compromise from text and files.
5
+ Author: ahur4
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://ahur4.github.io/Qarbaal/
8
+ Project-URL: Homepage, https://github.com/ahur4/Qarbaal
9
+ Project-URL: Source, https://github.com/ahur4/Qarbaal
10
+ Project-URL: Repository, https://github.com/ahur4/Qarbaal
11
+ Project-URL: Issues, https://github.com/ahur4/Qarbaal/issues
12
+ Project-URL: Changelog, https://github.com/ahur4/Qarbaal/blob/main/CHANGELOG.md
13
+ Keywords: ioc,threat-intelligence,dfir,cti,stix,indicators,qarbaal
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: html
27
+ Requires-Dist: beautifulsoup4>=4.12; extra == "html"
28
+ Provides-Extra: pdf
29
+ Requires-Dist: pymupdf>=1.24; extra == "pdf"
30
+ Provides-Extra: docx
31
+ Requires-Dist: python-docx>=1.1; extra == "docx"
32
+ Provides-Extra: yaml
33
+ Requires-Dist: pyyaml>=6.0; extra == "yaml"
34
+ Provides-Extra: pcap
35
+ Requires-Dist: scapy>=2.5; extra == "pcap"
36
+ Provides-Extra: pydantic
37
+ Requires-Dist: pydantic>=2.4; extra == "pydantic"
38
+ Provides-Extra: all
39
+ Requires-Dist: beautifulsoup4>=4.12; extra == "all"
40
+ Requires-Dist: pymupdf>=1.24; extra == "all"
41
+ Requires-Dist: python-docx>=1.1; extra == "all"
42
+ Requires-Dist: pyyaml>=6.0; extra == "all"
43
+ Requires-Dist: scapy>=2.5; extra == "all"
44
+ Requires-Dist: pydantic>=2.4; extra == "all"
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=8.0; extra == "dev"
47
+ Requires-Dist: mypy>=1.13; extra == "dev"
48
+ Requires-Dist: ruff>=0.8; extra == "dev"
49
+ Provides-Extra: docs
50
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
51
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
52
+ Dynamic: license-file
53
+
54
+ # qarbaal
55
+
56
+ Extract, normalize, validate, and export indicators of compromise from text and files.
57
+
58
+ [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776AB)](https://www.python.org/downloads/)
59
+ [![License: MIT](https://img.shields.io/github/license/ahur4/Qarbaal)](https://github.com/ahur4/Qarbaal/blob/main/LICENSE)
60
+ [![CI](https://img.shields.io/github/actions/workflow/status/ahur4/Qarbaal/ci.yml?branch=main&label=CI)](https://github.com/ahur4/Qarbaal/actions/workflows/ci.yml)
61
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-0A7AAA)](https://ahur4.github.io/Qarbaal/)
62
+
63
+ **Documentation:** <https://ahur4.github.io/Qarbaal/>
64
+
65
+ `qarbaal` reads reports, logs, and message files on your machine and returns classified indicators. It refangs defanged text, records where each value came from, and writes CSV, JSON, STIX 2.1, MISP, OpenIOC, and network rules. The base install uses only the Python standard library. It does not call an enrichment API.
66
+
67
+ ## Installation
68
+
69
+ Python 3.10 or newer.
70
+
71
+ ```console
72
+ pip install qarbaal
73
+ ```
74
+
75
+ That install has no runtime dependencies. Optional readers are separate extras:
76
+
77
+ ```console
78
+ pip install "qarbaal[html]" # HTML
79
+ pip install "qarbaal[pdf]" # PDF
80
+ pip install "qarbaal[docx]" # Word
81
+ pip install "qarbaal[yaml]" # YAML
82
+ pip install "qarbaal[pcap]" # packet captures
83
+ pip install "qarbaal[pydantic]"
84
+ pip install "qarbaal[all]"
85
+ ```
86
+
87
+ ## Quickstart
88
+
89
+ ```python
90
+ from qarbaal import IOCExtractor, extract
91
+
92
+ report = """
93
+ From: support[at]corp-it-desk[.]com
94
+ Lure: hxxps://login[.]corp-it-desk[.]com/session
95
+ Beacon: 185.220.101.42
96
+ MD5: d41d8cd98f00b204e9800998ecf8427e
97
+ """
98
+
99
+ result = extract(report)
100
+ for indicator in result:
101
+ print(indicator.type, indicator.value, indicator.tags, indicator.line)
102
+
103
+ extractor = IOCExtractor(
104
+ extract_types=["ipv4", "domain", "url", "hash", "email"],
105
+ normalize=True,
106
+ validate=True,
107
+ deduplicate=True,
108
+ )
109
+ same = extractor.extract(report, source="ticket-14")
110
+ ```
111
+
112
+ `extract()` uses the default type set. File paths, mutex names, and User-Agent strings stay out of that set until you ask for them with `extract_types` or `"all"`.
113
+
114
+ A synthetic incident is included:
115
+
116
+ ```python
117
+ from qarbaal import SAMPLE, extract
118
+
119
+ print(extract(SAMPLE).stats.counts)
120
+ ```
121
+
122
+ ## What you get back
123
+
124
+ Each indicator keeps three strings:
125
+
126
+ | Field | Meaning |
127
+ | --- | --- |
128
+ | `value` | First-seen sieve form. Hashes are lower case, CVE IDs are upper case. |
129
+ | `raw` | Exact slice of the source, which may still be defanged. |
130
+ | `normalized` | Canonical form used for the fingerprint. |
131
+
132
+ It also carries noise tags, a validation result, a confidence heuristic, local enrichment, pivot links, and one sighting per occurrence (source, line, offset, and page when the file has pages).
133
+
134
+ ## Command line
135
+
136
+ ```console
137
+ qarbaal report.txt
138
+ qarbaal mail.eml notes.json -f stix -o bundle.json
139
+ qarbaal --hide-private --defang -f table alerts.log
140
+ qarbaal --types all -f suricata capture.txt
141
+ ```
142
+
143
+ With no file, `qarbaal` reads stdin. Formats: `table`, `csv`, `json`, `jsonl`, `stix`, `list`, `misp`, `openioc`, `suricata`, `snort`.
144
+
145
+ ## Further reading
146
+
147
+ - [Installation](https://ahur4.github.io/Qarbaal/installation/)
148
+ - [Indicator types](https://ahur4.github.io/Qarbaal/indicator-types/)
149
+ - [Defanging](https://ahur4.github.io/Qarbaal/defanging/)
150
+ - [Normalization](https://ahur4.github.io/Qarbaal/normalization/)
151
+ - [Exports](https://ahur4.github.io/Qarbaal/exports/)
152
+ - [Command line](https://ahur4.github.io/Qarbaal/cli/)
153
+ - [API reference](https://ahur4.github.io/Qarbaal/reference/)
154
+ - [Changelog](https://ahur4.github.io/Qarbaal/changelog/)
@@ -0,0 +1,101 @@
1
+ # qarbaal
2
+
3
+ Extract, normalize, validate, and export indicators of compromise from text and files.
4
+
5
+ [![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-3776AB)](https://www.python.org/downloads/)
6
+ [![License: MIT](https://img.shields.io/github/license/ahur4/Qarbaal)](https://github.com/ahur4/Qarbaal/blob/main/LICENSE)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/ahur4/Qarbaal/ci.yml?branch=main&label=CI)](https://github.com/ahur4/Qarbaal/actions/workflows/ci.yml)
8
+ [![Documentation](https://img.shields.io/badge/docs-mkdocs-0A7AAA)](https://ahur4.github.io/Qarbaal/)
9
+
10
+ **Documentation:** <https://ahur4.github.io/Qarbaal/>
11
+
12
+ `qarbaal` reads reports, logs, and message files on your machine and returns classified indicators. It refangs defanged text, records where each value came from, and writes CSV, JSON, STIX 2.1, MISP, OpenIOC, and network rules. The base install uses only the Python standard library. It does not call an enrichment API.
13
+
14
+ ## Installation
15
+
16
+ Python 3.10 or newer.
17
+
18
+ ```console
19
+ pip install qarbaal
20
+ ```
21
+
22
+ That install has no runtime dependencies. Optional readers are separate extras:
23
+
24
+ ```console
25
+ pip install "qarbaal[html]" # HTML
26
+ pip install "qarbaal[pdf]" # PDF
27
+ pip install "qarbaal[docx]" # Word
28
+ pip install "qarbaal[yaml]" # YAML
29
+ pip install "qarbaal[pcap]" # packet captures
30
+ pip install "qarbaal[pydantic]"
31
+ pip install "qarbaal[all]"
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```python
37
+ from qarbaal import IOCExtractor, extract
38
+
39
+ report = """
40
+ From: support[at]corp-it-desk[.]com
41
+ Lure: hxxps://login[.]corp-it-desk[.]com/session
42
+ Beacon: 185.220.101.42
43
+ MD5: d41d8cd98f00b204e9800998ecf8427e
44
+ """
45
+
46
+ result = extract(report)
47
+ for indicator in result:
48
+ print(indicator.type, indicator.value, indicator.tags, indicator.line)
49
+
50
+ extractor = IOCExtractor(
51
+ extract_types=["ipv4", "domain", "url", "hash", "email"],
52
+ normalize=True,
53
+ validate=True,
54
+ deduplicate=True,
55
+ )
56
+ same = extractor.extract(report, source="ticket-14")
57
+ ```
58
+
59
+ `extract()` uses the default type set. File paths, mutex names, and User-Agent strings stay out of that set until you ask for them with `extract_types` or `"all"`.
60
+
61
+ A synthetic incident is included:
62
+
63
+ ```python
64
+ from qarbaal import SAMPLE, extract
65
+
66
+ print(extract(SAMPLE).stats.counts)
67
+ ```
68
+
69
+ ## What you get back
70
+
71
+ Each indicator keeps three strings:
72
+
73
+ | Field | Meaning |
74
+ | --- | --- |
75
+ | `value` | First-seen sieve form. Hashes are lower case, CVE IDs are upper case. |
76
+ | `raw` | Exact slice of the source, which may still be defanged. |
77
+ | `normalized` | Canonical form used for the fingerprint. |
78
+
79
+ It also carries noise tags, a validation result, a confidence heuristic, local enrichment, pivot links, and one sighting per occurrence (source, line, offset, and page when the file has pages).
80
+
81
+ ## Command line
82
+
83
+ ```console
84
+ qarbaal report.txt
85
+ qarbaal mail.eml notes.json -f stix -o bundle.json
86
+ qarbaal --hide-private --defang -f table alerts.log
87
+ qarbaal --types all -f suricata capture.txt
88
+ ```
89
+
90
+ With no file, `qarbaal` reads stdin. Formats: `table`, `csv`, `json`, `jsonl`, `stix`, `list`, `misp`, `openioc`, `suricata`, `snort`.
91
+
92
+ ## Further reading
93
+
94
+ - [Installation](https://ahur4.github.io/Qarbaal/installation/)
95
+ - [Indicator types](https://ahur4.github.io/Qarbaal/indicator-types/)
96
+ - [Defanging](https://ahur4.github.io/Qarbaal/defanging/)
97
+ - [Normalization](https://ahur4.github.io/Qarbaal/normalization/)
98
+ - [Exports](https://ahur4.github.io/Qarbaal/exports/)
99
+ - [Command line](https://ahur4.github.io/Qarbaal/cli/)
100
+ - [API reference](https://ahur4.github.io/Qarbaal/reference/)
101
+ - [Changelog](https://ahur4.github.io/Qarbaal/changelog/)
@@ -0,0 +1,102 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "qarbaal"
7
+ version = "0.1.0"
8
+ description = "Extract, normalize, validate, and export indicators of compromise from text and files."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "ahur4" }]
14
+ keywords = [
15
+ "ioc",
16
+ "threat-intelligence",
17
+ "dfir",
18
+ "cti",
19
+ "stix",
20
+ "indicators",
21
+ "qarbaal",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Security",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = []
35
+
36
+ [project.optional-dependencies]
37
+ html = ["beautifulsoup4>=4.12"]
38
+ pdf = ["pymupdf>=1.24"]
39
+ docx = ["python-docx>=1.1"]
40
+ yaml = ["pyyaml>=6.0"]
41
+ pcap = ["scapy>=2.5"]
42
+ pydantic = ["pydantic>=2.4"]
43
+ all = [
44
+ "beautifulsoup4>=4.12",
45
+ "pymupdf>=1.24",
46
+ "python-docx>=1.1",
47
+ "pyyaml>=6.0",
48
+ "scapy>=2.5",
49
+ "pydantic>=2.4",
50
+ ]
51
+ dev = [
52
+ "pytest>=8.0",
53
+ "mypy>=1.13",
54
+ "ruff>=0.8",
55
+ ]
56
+ docs = [
57
+ "mkdocs-material>=9.5",
58
+ "mkdocstrings[python]>=0.26",
59
+ ]
60
+
61
+ [project.urls]
62
+ Documentation = "https://ahur4.github.io/Qarbaal/"
63
+ Homepage = "https://github.com/ahur4/Qarbaal"
64
+ Source = "https://github.com/ahur4/Qarbaal"
65
+ Repository = "https://github.com/ahur4/Qarbaal"
66
+ Issues = "https://github.com/ahur4/Qarbaal/issues"
67
+ Changelog = "https://github.com/ahur4/Qarbaal/blob/main/CHANGELOG.md"
68
+
69
+ [project.scripts]
70
+ qarbaal = "qarbaal.cli:main"
71
+
72
+ [tool.setuptools.packages.find]
73
+ include = ["qarbaal*"]
74
+
75
+ [tool.setuptools.package-data]
76
+ qarbaal = ["py.typed", "data/*"]
77
+
78
+ [tool.pytest.ini_options]
79
+ testpaths = ["tests"]
80
+
81
+ [tool.ruff]
82
+ target-version = "py310"
83
+ line-length = 100
84
+ src = ["qarbaal", "tests"]
85
+
86
+ [tool.ruff.lint]
87
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "C4"]
88
+
89
+ [tool.ruff.lint.isort]
90
+ known-first-party = ["qarbaal"]
91
+
92
+ [tool.ruff.lint.per-file-ignores]
93
+ "qarbaal/ingest.py" = ["B009"]
94
+ "qarbaal/adapters.py" = ["B009"]
95
+ "qarbaal/patterns.py" = ["SIM905"]
96
+ "qarbaal/sample.py" = ["E501"]
97
+
98
+ [tool.mypy]
99
+ python_version = "3.10"
100
+ strict = true
101
+ packages = ["qarbaal"]
102
+ warn_unused_ignores = true
@@ -0,0 +1,77 @@
1
+ """Extract, normalize, and export indicators of compromise.
2
+
3
+ ``qarbaal`` reads text and common file formats locally. It does not call a
4
+ network service. Pivot URLs are built for the analyst to open.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from qarbaal.exceptions import (
10
+ InputTooLargeError,
11
+ OptionalDependencyError,
12
+ ParseError,
13
+ QarbaalError,
14
+ UnsupportedFormatError,
15
+ )
16
+ from qarbaal.export import (
17
+ to_csv,
18
+ to_defanged_list,
19
+ to_json,
20
+ to_jsonl,
21
+ to_misp,
22
+ to_openioc,
23
+ to_snort,
24
+ to_stix,
25
+ to_suricata,
26
+ )
27
+ from qarbaal.extract import IOCExtractor, extract, merge
28
+ from qarbaal.ingest import LoadedDocument, load_document
29
+ from qarbaal.normalize import normalize_value
30
+ from qarbaal.patterns import PatternSpec
31
+ from qarbaal.refang import defang, refang
32
+ from qarbaal.sample import SAMPLE
33
+ from qarbaal.types import (
34
+ Enrichment,
35
+ Extraction,
36
+ ExtractionStats,
37
+ Indicator,
38
+ IOCType,
39
+ Sighting,
40
+ )
41
+ from qarbaal.validate import ipv4_class
42
+ from qarbaal.version import __version__
43
+
44
+ __all__ = [
45
+ "SAMPLE",
46
+ "Enrichment",
47
+ "Extraction",
48
+ "ExtractionStats",
49
+ "IOCExtractor",
50
+ "IOCType",
51
+ "Indicator",
52
+ "InputTooLargeError",
53
+ "LoadedDocument",
54
+ "OptionalDependencyError",
55
+ "ParseError",
56
+ "PatternSpec",
57
+ "QarbaalError",
58
+ "Sighting",
59
+ "UnsupportedFormatError",
60
+ "__version__",
61
+ "defang",
62
+ "extract",
63
+ "ipv4_class",
64
+ "load_document",
65
+ "merge",
66
+ "normalize_value",
67
+ "refang",
68
+ "to_csv",
69
+ "to_defanged_list",
70
+ "to_json",
71
+ "to_jsonl",
72
+ "to_misp",
73
+ "to_openioc",
74
+ "to_snort",
75
+ "to_stix",
76
+ "to_suricata",
77
+ ]
@@ -0,0 +1,7 @@
1
+ """``python -m qarbaal``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from qarbaal.cli import main
6
+
7
+ raise SystemExit(main())
@@ -0,0 +1,98 @@
1
+ """Optional pydantic models.
2
+
3
+ The core result types are dataclasses. This module builds a pydantic model only
4
+ when pydantic is installed (``pip install 'qarbaal[pydantic]'``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from collections.abc import Callable
11
+ from functools import lru_cache
12
+ from typing import Protocol, cast
13
+
14
+ from qarbaal.exceptions import OptionalDependencyError
15
+ from qarbaal.export import indicator_to_dict
16
+ from qarbaal.types import Extraction, Indicator
17
+
18
+
19
+ class IndicatorRecord(Protocol):
20
+ """Structural type of the optional pydantic indicator model."""
21
+
22
+ type: str
23
+ value: str
24
+ normalized: str
25
+ raw: str
26
+ fingerprint: str
27
+ confidence: float
28
+ occurrences: int
29
+ valid: bool | None
30
+
31
+
32
+ def to_pydantic(indicator: Indicator) -> IndicatorRecord:
33
+ """Convert one indicator to a pydantic model.
34
+
35
+ Args:
36
+ indicator: Indicator to convert.
37
+
38
+ Returns:
39
+ A pydantic ``BaseModel`` instance with the indicator fields.
40
+
41
+ Raises:
42
+ OptionalDependencyError: pydantic is not installed.
43
+ """
44
+
45
+ validate = _validator()
46
+ return cast(IndicatorRecord, validate(indicator_to_dict(indicator)))
47
+
48
+
49
+ def extraction_to_pydantic(result: Extraction) -> list[IndicatorRecord]:
50
+ """Convert every indicator in an extraction.
51
+
52
+ Args:
53
+ result: Extraction to convert.
54
+
55
+ Returns:
56
+ One pydantic model per indicator.
57
+
58
+ Raises:
59
+ OptionalDependencyError: pydantic is not installed.
60
+ """
61
+
62
+ return [to_pydantic(indicator) for indicator in result]
63
+
64
+
65
+ @lru_cache(maxsize=1)
66
+ def _validator() -> Callable[[object], object]:
67
+ try:
68
+ module = importlib.import_module("pydantic")
69
+ except ImportError as exc:
70
+ raise OptionalDependencyError(
71
+ "pydantic is required for this adapter. "
72
+ "Install it with: pip install 'qarbaal[pydantic]'"
73
+ ) from exc
74
+ factory = getattr(module, "create_model", None)
75
+ if not callable(factory):
76
+ raise OptionalDependencyError("pydantic.create_model is unavailable")
77
+ create = cast(Callable[..., type[object]], factory)
78
+ model = create(
79
+ "QarbaalIndicator",
80
+ type=(str, ...),
81
+ value=(str, ...),
82
+ normalized=(str, ...),
83
+ raw=(str, ...),
84
+ tags=(list[str], ...),
85
+ fingerprint=(str, ...),
86
+ valid=(bool | None, ...),
87
+ confidence=(float, ...),
88
+ validation_notes=(list[str], ...),
89
+ occurrences=(int, ...),
90
+ sources=(list[str], ...),
91
+ sightings=(list[dict[str, object]], ...),
92
+ enrichment=(dict[str, object], ...),
93
+ pivots=(list[dict[str, str]], ...),
94
+ )
95
+ validate = getattr(model, "model_validate", None)
96
+ if not callable(validate):
97
+ raise OptionalDependencyError("pydantic model has no model_validate")
98
+ return cast(Callable[[object], object], validate)