classifyre-cli 0.4.2__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.
- classifyre_cli-0.4.2.dist-info/METADATA +167 -0
- classifyre_cli-0.4.2.dist-info/RECORD +101 -0
- classifyre_cli-0.4.2.dist-info/WHEEL +4 -0
- classifyre_cli-0.4.2.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/detectors/__init__.py +105 -0
- src/detectors/base.py +97 -0
- src/detectors/broken_links/__init__.py +3 -0
- src/detectors/broken_links/detector.py +280 -0
- src/detectors/config.py +59 -0
- src/detectors/content/__init__.py +0 -0
- src/detectors/custom/__init__.py +13 -0
- src/detectors/custom/detector.py +45 -0
- src/detectors/custom/runners/__init__.py +56 -0
- src/detectors/custom/runners/_base.py +177 -0
- src/detectors/custom/runners/_factory.py +51 -0
- src/detectors/custom/runners/_feature_extraction.py +138 -0
- src/detectors/custom/runners/_gliner2.py +324 -0
- src/detectors/custom/runners/_image_classification.py +98 -0
- src/detectors/custom/runners/_llm.py +22 -0
- src/detectors/custom/runners/_object_detection.py +107 -0
- src/detectors/custom/runners/_regex.py +147 -0
- src/detectors/custom/runners/_text_classification.py +109 -0
- src/detectors/custom/trainer.py +293 -0
- src/detectors/dependencies.py +109 -0
- src/detectors/pii/__init__.py +0 -0
- src/detectors/pii/detector.py +883 -0
- src/detectors/secrets/__init__.py +0 -0
- src/detectors/secrets/detector.py +399 -0
- src/detectors/threat/__init__.py +0 -0
- src/detectors/threat/code_security_detector.py +206 -0
- src/detectors/threat/yara_detector.py +177 -0
- src/main.py +608 -0
- src/models/generated_detectors.py +1296 -0
- src/models/generated_input.py +2732 -0
- src/models/generated_single_asset_scan_results.py +240 -0
- src/outputs/__init__.py +3 -0
- src/outputs/base.py +69 -0
- src/outputs/console.py +62 -0
- src/outputs/factory.py +156 -0
- src/outputs/file.py +83 -0
- src/outputs/rest.py +258 -0
- src/pipeline/__init__.py +7 -0
- src/pipeline/content_provider.py +26 -0
- src/pipeline/detector_pipeline.py +742 -0
- src/pipeline/parsed_content_provider.py +59 -0
- src/sandbox/__init__.py +5 -0
- src/sandbox/runner.py +145 -0
- src/sources/__init__.py +95 -0
- src/sources/atlassian_common.py +389 -0
- src/sources/azure_blob_storage/__init__.py +3 -0
- src/sources/azure_blob_storage/source.py +130 -0
- src/sources/base.py +296 -0
- src/sources/confluence/__init__.py +3 -0
- src/sources/confluence/source.py +733 -0
- src/sources/databricks/__init__.py +3 -0
- src/sources/databricks/source.py +1279 -0
- src/sources/dependencies.py +81 -0
- src/sources/google_cloud_storage/__init__.py +3 -0
- src/sources/google_cloud_storage/source.py +114 -0
- src/sources/hive/__init__.py +3 -0
- src/sources/hive/source.py +709 -0
- src/sources/jira/__init__.py +3 -0
- src/sources/jira/source.py +605 -0
- src/sources/mongodb/__init__.py +3 -0
- src/sources/mongodb/source.py +550 -0
- src/sources/mssql/__init__.py +3 -0
- src/sources/mssql/source.py +1034 -0
- src/sources/mysql/__init__.py +3 -0
- src/sources/mysql/source.py +797 -0
- src/sources/neo4j/__init__.py +0 -0
- src/sources/neo4j/source.py +523 -0
- src/sources/object_storage/base.py +679 -0
- src/sources/oracle/__init__.py +3 -0
- src/sources/oracle/source.py +982 -0
- src/sources/postgresql/__init__.py +3 -0
- src/sources/postgresql/source.py +774 -0
- src/sources/powerbi/__init__.py +3 -0
- src/sources/powerbi/source.py +774 -0
- src/sources/recipe_normalizer.py +179 -0
- src/sources/s3_compatible_storage/README.md +66 -0
- src/sources/s3_compatible_storage/__init__.py +3 -0
- src/sources/s3_compatible_storage/source.py +150 -0
- src/sources/servicedesk/__init__.py +3 -0
- src/sources/servicedesk/source.py +620 -0
- src/sources/slack/__init__.py +3 -0
- src/sources/slack/source.py +534 -0
- src/sources/snowflake/__init__.py +3 -0
- src/sources/snowflake/source.py +912 -0
- src/sources/tableau/__init__.py +3 -0
- src/sources/tableau/source.py +799 -0
- src/sources/tabular_utils.py +165 -0
- src/sources/wordpress/__init__.py +3 -0
- src/sources/wordpress/source.py +590 -0
- src/telemetry.py +96 -0
- src/utils/__init__.py +1 -0
- src/utils/content_extraction.py +108 -0
- src/utils/file_parser.py +777 -0
- src/utils/hashing.py +82 -0
- src/utils/uv_sync.py +79 -0
- src/utils/validation.py +56 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""YARA-based threat detector — compiles structured rule objects into a live ruleset."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from ...models.generated_detectors import (
|
|
7
|
+
DetectorConfig,
|
|
8
|
+
Severity,
|
|
9
|
+
ThreatDetectorConfig,
|
|
10
|
+
YaraRuleConfig,
|
|
11
|
+
)
|
|
12
|
+
from ...models.generated_single_asset_scan_results import DetectionResult, DetectorType, Location
|
|
13
|
+
from ..base import BaseDetector
|
|
14
|
+
from ..dependencies import require_module
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_SEVERITY_MAP: dict[str, Severity] = {
|
|
19
|
+
"critical": Severity.critical,
|
|
20
|
+
"high": Severity.high,
|
|
21
|
+
"medium": Severity.medium,
|
|
22
|
+
"low": Severity.low,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_SEVERITY_ORDER: dict[str, int] = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
26
|
+
|
|
27
|
+
_SAFE_NAME = re.compile(r"[^A-Za-z0-9_]")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sanitize_name(name: str) -> str:
|
|
31
|
+
sanitized = _SAFE_NAME.sub("_", name)
|
|
32
|
+
return ("_" + sanitized) if sanitized and sanitized[0].isdigit() else sanitized or "Rule"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_source(rules: list[YaraRuleConfig]) -> str:
|
|
36
|
+
parts: list[str] = []
|
|
37
|
+
for rule in rules:
|
|
38
|
+
strings_block = "\n".join(
|
|
39
|
+
f" $s{i} = {pattern}" for i, pattern in enumerate(rule.strings)
|
|
40
|
+
)
|
|
41
|
+
desc = (rule.description or "").replace('"', '\\"')
|
|
42
|
+
sev = rule.severity.value if hasattr(rule.severity, "value") else str(rule.severity)
|
|
43
|
+
cat = (rule.category or "").replace('"', '\\"')
|
|
44
|
+
parts.append(
|
|
45
|
+
f"rule {_sanitize_name(rule.name)} {{\n"
|
|
46
|
+
f" meta:\n"
|
|
47
|
+
f' description = "{desc}"\n'
|
|
48
|
+
f' severity = "{sev}"\n'
|
|
49
|
+
f' category = "{cat}"\n'
|
|
50
|
+
f" strings:\n"
|
|
51
|
+
f"{strings_block}\n"
|
|
52
|
+
f" condition:\n"
|
|
53
|
+
f" {rule.condition}\n"
|
|
54
|
+
f"}}"
|
|
55
|
+
)
|
|
56
|
+
return "\n\n".join(parts)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class YaraDetector(BaseDetector):
|
|
60
|
+
"""
|
|
61
|
+
Threat detector powered by yara-python.
|
|
62
|
+
|
|
63
|
+
Takes structured rule objects from config, compiles them into a YARA ruleset,
|
|
64
|
+
and scans extracted text or raw bytes for matches. Use the bundled examples in
|
|
65
|
+
all_detectors_examples.json as starting points and extend with custom rules.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
detector_type = "yara"
|
|
69
|
+
detector_name = "yara"
|
|
70
|
+
|
|
71
|
+
def __init__(self, config: DetectorConfig | None = None) -> None:
|
|
72
|
+
super().__init__(config)
|
|
73
|
+
self._yara = require_module("yara", "yara", ["security"])
|
|
74
|
+
self._threat_config = (
|
|
75
|
+
config if isinstance(config, ThreatDetectorConfig) else ThreatDetectorConfig()
|
|
76
|
+
)
|
|
77
|
+
self._rules = self._compile()
|
|
78
|
+
|
|
79
|
+
def _compile(self) -> object | None:
|
|
80
|
+
rules = self._threat_config.rules
|
|
81
|
+
if not rules:
|
|
82
|
+
return None
|
|
83
|
+
source = _build_source(rules)
|
|
84
|
+
try:
|
|
85
|
+
return self._yara.compile(source=source)
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.exception("YARA compilation failed")
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
async def detect(
|
|
91
|
+
self, content: str | bytes, content_type: str = "text/plain"
|
|
92
|
+
) -> list[DetectionResult]:
|
|
93
|
+
if self._rules is None:
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
data = content if isinstance(content, bytes) else content.encode("utf-8", errors="ignore")
|
|
97
|
+
timeout = self._threat_config.timeout or 60
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
matches = self._rules.match(data=data, timeout=timeout)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
if "timeout" in str(exc).lower():
|
|
103
|
+
logger.warning("YARA scan timed out after %ds on %s", timeout, content_type)
|
|
104
|
+
else:
|
|
105
|
+
logger.error("YARA scan error on %s: %s", content_type, exc)
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
threshold = self._threat_config.confidence_threshold or 0.7
|
|
109
|
+
results: list[DetectionResult] = []
|
|
110
|
+
|
|
111
|
+
for match in matches:
|
|
112
|
+
meta: dict[str, object] = getattr(match, "meta", {}) or {}
|
|
113
|
+
rule_name = str(getattr(match, "rule", "unknown"))
|
|
114
|
+
description = str(meta.get("description", rule_name))
|
|
115
|
+
severity = _SEVERITY_MAP.get(str(meta.get("severity", "medium")), Severity.medium)
|
|
116
|
+
|
|
117
|
+
matched_texts = [
|
|
118
|
+
inst.matched_data.decode("utf-8", errors="replace")
|
|
119
|
+
for sm in getattr(match, "strings", [])
|
|
120
|
+
for inst in getattr(sm, "instances", [])
|
|
121
|
+
]
|
|
122
|
+
count = len(matched_texts)
|
|
123
|
+
confidence = min(0.70 + max(count - 1, 0) * 0.04, 0.99)
|
|
124
|
+
if confidence < threshold:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
results.append(
|
|
128
|
+
DetectionResult(
|
|
129
|
+
detector_type=DetectorType.YARA,
|
|
130
|
+
finding_type=rule_name,
|
|
131
|
+
category="THREAT",
|
|
132
|
+
severity=severity,
|
|
133
|
+
confidence=confidence,
|
|
134
|
+
matched_content=", ".join(matched_texts[:3]),
|
|
135
|
+
location=Location(
|
|
136
|
+
path=f"yara:{content_type}",
|
|
137
|
+
description=description,
|
|
138
|
+
),
|
|
139
|
+
metadata={
|
|
140
|
+
"rule": rule_name,
|
|
141
|
+
"description": description,
|
|
142
|
+
"match_count": count,
|
|
143
|
+
"tags": list(getattr(match, "tags", [])),
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
results.sort(
|
|
149
|
+
key=lambda r: (_SEVERITY_ORDER.get(r.severity.value, 0), r.confidence),
|
|
150
|
+
reverse=True,
|
|
151
|
+
)
|
|
152
|
+
max_f = self._threat_config.max_findings
|
|
153
|
+
return results[:max_f] if max_f and len(results) > max_f else results
|
|
154
|
+
|
|
155
|
+
def get_supported_content_types(self) -> list[str]:
|
|
156
|
+
return [
|
|
157
|
+
"text/plain",
|
|
158
|
+
"text/html",
|
|
159
|
+
"text/csv",
|
|
160
|
+
"text/markdown",
|
|
161
|
+
"text/x-python",
|
|
162
|
+
"text/x-shellscript",
|
|
163
|
+
"text/javascript",
|
|
164
|
+
"application/json",
|
|
165
|
+
"application/xml",
|
|
166
|
+
"application/pdf",
|
|
167
|
+
"application/octet-stream",
|
|
168
|
+
"application/x-sh",
|
|
169
|
+
"application/x-executable",
|
|
170
|
+
"application/javascript",
|
|
171
|
+
"application/vnd.ms-excel",
|
|
172
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
173
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
def requires_gpu(self) -> bool:
|
|
177
|
+
return False
|