clickdetect 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.
- clickdetect-0.1.0/PKG-INFO +78 -0
- clickdetect-0.1.0/README.md +62 -0
- clickdetect-0.1.0/api/detector.py +70 -0
- clickdetect-0.1.0/api/health.py +6 -0
- clickdetect-0.1.0/api/rules.py +45 -0
- clickdetect-0.1.0/clickdetect.egg-info/PKG-INFO +78 -0
- clickdetect-0.1.0/clickdetect.egg-info/SOURCES.txt +31 -0
- clickdetect-0.1.0/clickdetect.egg-info/dependency_links.txt +1 -0
- clickdetect-0.1.0/clickdetect.egg-info/entry_points.txt +2 -0
- clickdetect-0.1.0/clickdetect.egg-info/requires.txt +9 -0
- clickdetect-0.1.0/clickdetect.egg-info/top_level.txt +3 -0
- clickdetect-0.1.0/clickdetect.py +79 -0
- clickdetect-0.1.0/detector/__init__.py +1 -0
- clickdetect-0.1.0/detector/config.py +36 -0
- clickdetect-0.1.0/detector/datasource/__init__.py +14 -0
- clickdetect-0.1.0/detector/datasource/base.py +31 -0
- clickdetect-0.1.0/detector/datasource/clickhouse.py +76 -0
- clickdetect-0.1.0/detector/datasource/elasticsearch.py +109 -0
- clickdetect-0.1.0/detector/datasource/loki.py +125 -0
- clickdetect-0.1.0/detector/datasource/postgresql.py +73 -0
- clickdetect-0.1.0/detector/detector.py +218 -0
- clickdetect-0.1.0/detector/manager.py +86 -0
- clickdetect-0.1.0/detector/rules.py +61 -0
- clickdetect-0.1.0/detector/runner.py +129 -0
- clickdetect-0.1.0/detector/utils.py +58 -0
- clickdetect-0.1.0/detector/webhooks/__init__.py +14 -0
- clickdetect-0.1.0/detector/webhooks/base.py +25 -0
- clickdetect-0.1.0/detector/webhooks/email.py +113 -0
- clickdetect-0.1.0/detector/webhooks/generic.py +70 -0
- clickdetect-0.1.0/detector/webhooks/matrix.py +97 -0
- clickdetect-0.1.0/detector/webhooks/teams.py +86 -0
- clickdetect-0.1.0/pyproject.toml +38 -0
- clickdetect-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clickdetect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic SIEM detector
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: aiohttp[speedups]>=3.13.2
|
|
8
|
+
Requires-Dist: apscheduler>=3.11.1
|
|
9
|
+
Requires-Dist: asyncpg>=0.31.0
|
|
10
|
+
Requires-Dist: clickhouse-connect>=0.10.0
|
|
11
|
+
Requires-Dist: colorlog>=6.10.1
|
|
12
|
+
Requires-Dist: fastapi[standard]>=0.127.1
|
|
13
|
+
Requires-Dist: jinja2>=3.1.6
|
|
14
|
+
Requires-Dist: matrix-nio[e2e]>=0.25.2
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
16
|
+
|
|
17
|
+
# Overview
|
|
18
|
+
|
|
19
|
+
**Clickdetect** is a framework for threshold-based detection and alerting. It periodically queries your data sources, evaluates rules against the results, and sends alerts to one or more destinations when conditions are met.
|
|
20
|
+
|
|
21
|
+
You can pull events from any DataSource implemented, and push alerts to any webhook.
|
|
22
|
+
|
|
23
|
+
If you use elastalert, you will like this!
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
Documentation: [https://clickdetect.souzo.me](https://clickdetect.souzo.me)
|
|
28
|
+
|
|
29
|
+
## Next steps
|
|
30
|
+
|
|
31
|
+
* Implement timeframe []
|
|
32
|
+
* Grouping alerts []
|
|
33
|
+
* Suppress alerts []
|
|
34
|
+
* Hot reload rules []
|
|
35
|
+
* Add new rules using api []
|
|
36
|
+
* Add api endpoint to silence detectors []
|
|
37
|
+
* Sigma converter in rule (sigma: true) []
|
|
38
|
+
* Sync schedulers for scalability []
|
|
39
|
+
* Get rules from S3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Using uv
|
|
45
|
+
|
|
46
|
+
Download here:
|
|
47
|
+
|
|
48
|
+
1. https://docs.astral.sh/uv/getting-started/installation/
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
52
|
+
cd clickdetect
|
|
53
|
+
uv sync --no-dev
|
|
54
|
+
uv run clickdetect
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using Docker / Podman
|
|
58
|
+
|
|
59
|
+
From repository
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
63
|
+
cd clickdetect
|
|
64
|
+
podman build -t clickdetect .
|
|
65
|
+
podman run --rm -v ./runner.yml:/app/runner.yml -p 8080:8080 clickdetect --api
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Contribution
|
|
69
|
+
|
|
70
|
+
* Like this project
|
|
71
|
+
* Help me to create a sigma converter for clickhouse.
|
|
72
|
+
* Report bugs in the issues
|
|
73
|
+
|
|
74
|
+
## Contact
|
|
75
|
+
|
|
76
|
+
* E-mail: me@souzo.me <vinicius morais>
|
|
77
|
+
* [Linkedin](https://www.linkedin.com/in/vinicius-f-a76ba51b5/)
|
|
78
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
**Clickdetect** is a framework for threshold-based detection and alerting. It periodically queries your data sources, evaluates rules against the results, and sends alerts to one or more destinations when conditions are met.
|
|
4
|
+
|
|
5
|
+
You can pull events from any DataSource implemented, and push alerts to any webhook.
|
|
6
|
+
|
|
7
|
+
If you use elastalert, you will like this!
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
Documentation: [https://clickdetect.souzo.me](https://clickdetect.souzo.me)
|
|
12
|
+
|
|
13
|
+
## Next steps
|
|
14
|
+
|
|
15
|
+
* Implement timeframe []
|
|
16
|
+
* Grouping alerts []
|
|
17
|
+
* Suppress alerts []
|
|
18
|
+
* Hot reload rules []
|
|
19
|
+
* Add new rules using api []
|
|
20
|
+
* Add api endpoint to silence detectors []
|
|
21
|
+
* Sigma converter in rule (sigma: true) []
|
|
22
|
+
* Sync schedulers for scalability []
|
|
23
|
+
* Get rules from S3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
### Using uv
|
|
29
|
+
|
|
30
|
+
Download here:
|
|
31
|
+
|
|
32
|
+
1. https://docs.astral.sh/uv/getting-started/installation/
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
36
|
+
cd clickdetect
|
|
37
|
+
uv sync --no-dev
|
|
38
|
+
uv run clickdetect
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Using Docker / Podman
|
|
42
|
+
|
|
43
|
+
From repository
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
47
|
+
cd clickdetect
|
|
48
|
+
podman build -t clickdetect .
|
|
49
|
+
podman run --rm -v ./runner.yml:/app/runner.yml -p 8080:8080 clickdetect --api
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Contribution
|
|
53
|
+
|
|
54
|
+
* Like this project
|
|
55
|
+
* Help me to create a sigma converter for clickhouse.
|
|
56
|
+
* Report bugs in the issues
|
|
57
|
+
|
|
58
|
+
## Contact
|
|
59
|
+
|
|
60
|
+
* E-mail: me@souzo.me <vinicius morais>
|
|
61
|
+
* [Linkedin](https://www.linkedin.com/in/vinicius-f-a76ba51b5/)
|
|
62
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from detector.manager import get_manager_instance
|
|
3
|
+
from detector.detector import Detector
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix='/detector')
|
|
7
|
+
|
|
8
|
+
def detector_to_dict(job_id: str, d: Detector):
|
|
9
|
+
return {
|
|
10
|
+
'id': job_id,
|
|
11
|
+
'name': d.name,
|
|
12
|
+
'description': d.description,
|
|
13
|
+
'tenant': d.tenant,
|
|
14
|
+
'active': d.active,
|
|
15
|
+
'for_time': d.for_time,
|
|
16
|
+
'rules_count': len(d._rules),
|
|
17
|
+
'webhooks': d.webhooks,
|
|
18
|
+
'last_time_exec': datetime.fromtimestamp(d._last_time).isoformat(),
|
|
19
|
+
'next_time_exec': datetime.fromtimestamp(d._next_time).isoformat()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get('/list')
|
|
24
|
+
async def listDetectors():
|
|
25
|
+
manager = get_manager_instance()
|
|
26
|
+
detectors = await manager.get_detectors()
|
|
27
|
+
return [detector_to_dict(job_id, d) for job_id, d in detectors.items()]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get('/tenant/{tenant}')
|
|
31
|
+
async def getDetectorsByTenant(tenant: str):
|
|
32
|
+
manager = get_manager_instance()
|
|
33
|
+
detectors = await manager.get_detectors()
|
|
34
|
+
return [detector_to_dict(job_id, d) for job_id, d in detectors.items() if d.tenant == tenant]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get('/{id}')
|
|
38
|
+
async def getDetector(id: str):
|
|
39
|
+
manager = get_manager_instance()
|
|
40
|
+
detector = await manager.get_detector_by_id(id)
|
|
41
|
+
if not detector:
|
|
42
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
43
|
+
return detector_to_dict(id, detector)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.delete('/{id}')
|
|
47
|
+
async def deleteDetector(id: str):
|
|
48
|
+
manager = get_manager_instance()
|
|
49
|
+
result = await manager.remove_scheduler(id)
|
|
50
|
+
if not result:
|
|
51
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
52
|
+
return {'deleted': id}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.post('/{id}/stop')
|
|
56
|
+
async def stopDetector(id: str):
|
|
57
|
+
manager = get_manager_instance()
|
|
58
|
+
result = await manager.stop_scheduler(id)
|
|
59
|
+
if not result:
|
|
60
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
61
|
+
return {'stopped': id}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.post('/{id}/resume')
|
|
65
|
+
async def resumeDetector(id: str):
|
|
66
|
+
manager = get_manager_instance()
|
|
67
|
+
result = await manager.resume_scheduler(id)
|
|
68
|
+
if not result:
|
|
69
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
70
|
+
return {'resumed': id}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from detector.manager import get_manager_instance
|
|
3
|
+
router = APIRouter(prefix='/rules')
|
|
4
|
+
|
|
5
|
+
@router.get('/{detector_id}')
|
|
6
|
+
async def listRules(detector_id: str):
|
|
7
|
+
manager = get_manager_instance()
|
|
8
|
+
detector = await manager.get_detector_by_id(detector_id)
|
|
9
|
+
if not detector:
|
|
10
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
11
|
+
return [x.to_dict() for x in detector._rules]
|
|
12
|
+
|
|
13
|
+
@router.get('/{detector_id}/{rule_id}')
|
|
14
|
+
async def getRuleById(detector_id: str, rule_id: str):
|
|
15
|
+
manager = get_manager_instance()
|
|
16
|
+
detector = await manager.get_detector_by_id(detector_id)
|
|
17
|
+
if not detector:
|
|
18
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
19
|
+
rule = await detector.get_rule_by_id(rule_id)
|
|
20
|
+
if not rule:
|
|
21
|
+
raise HTTPException(status_code=404, detail='Rule not found')
|
|
22
|
+
return rule.to_dict()
|
|
23
|
+
|
|
24
|
+
@router.get('/{detector_id}/{rule_id}/pause')
|
|
25
|
+
async def pauseRule(detector_id: str, rule_id: str):
|
|
26
|
+
manager = get_manager_instance()
|
|
27
|
+
detector = await manager.get_detector_by_id(detector_id)
|
|
28
|
+
if not detector:
|
|
29
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
30
|
+
ok = await detector.setRuleActive(rule_id, False)
|
|
31
|
+
if not ok:
|
|
32
|
+
raise HTTPException(status_code=404, detail='Rule not found')
|
|
33
|
+
return { 'ok': True }
|
|
34
|
+
|
|
35
|
+
@router.get('/{detector_id}/{rule_id}/resume')
|
|
36
|
+
async def resumeRule(detector_id: str, rule_id: str):
|
|
37
|
+
manager = get_manager_instance()
|
|
38
|
+
detector = await manager.get_detector_by_id(detector_id)
|
|
39
|
+
if not detector:
|
|
40
|
+
raise HTTPException(status_code=404, detail='Detector not found')
|
|
41
|
+
ok = await detector.setRuleActive(rule_id, True)
|
|
42
|
+
if not ok:
|
|
43
|
+
raise HTTPException(status_code=404, detail='Rule not found')
|
|
44
|
+
return { 'ok': True }
|
|
45
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clickdetect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generic SIEM detector
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: aiohttp[speedups]>=3.13.2
|
|
8
|
+
Requires-Dist: apscheduler>=3.11.1
|
|
9
|
+
Requires-Dist: asyncpg>=0.31.0
|
|
10
|
+
Requires-Dist: clickhouse-connect>=0.10.0
|
|
11
|
+
Requires-Dist: colorlog>=6.10.1
|
|
12
|
+
Requires-Dist: fastapi[standard]>=0.127.1
|
|
13
|
+
Requires-Dist: jinja2>=3.1.6
|
|
14
|
+
Requires-Dist: matrix-nio[e2e]>=0.25.2
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
16
|
+
|
|
17
|
+
# Overview
|
|
18
|
+
|
|
19
|
+
**Clickdetect** is a framework for threshold-based detection and alerting. It periodically queries your data sources, evaluates rules against the results, and sends alerts to one or more destinations when conditions are met.
|
|
20
|
+
|
|
21
|
+
You can pull events from any DataSource implemented, and push alerts to any webhook.
|
|
22
|
+
|
|
23
|
+
If you use elastalert, you will like this!
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
Documentation: [https://clickdetect.souzo.me](https://clickdetect.souzo.me)
|
|
28
|
+
|
|
29
|
+
## Next steps
|
|
30
|
+
|
|
31
|
+
* Implement timeframe []
|
|
32
|
+
* Grouping alerts []
|
|
33
|
+
* Suppress alerts []
|
|
34
|
+
* Hot reload rules []
|
|
35
|
+
* Add new rules using api []
|
|
36
|
+
* Add api endpoint to silence detectors []
|
|
37
|
+
* Sigma converter in rule (sigma: true) []
|
|
38
|
+
* Sync schedulers for scalability []
|
|
39
|
+
* Get rules from S3
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Using uv
|
|
45
|
+
|
|
46
|
+
Download here:
|
|
47
|
+
|
|
48
|
+
1. https://docs.astral.sh/uv/getting-started/installation/
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
52
|
+
cd clickdetect
|
|
53
|
+
uv sync --no-dev
|
|
54
|
+
uv run clickdetect
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using Docker / Podman
|
|
58
|
+
|
|
59
|
+
From repository
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
git clone https://github.com/clicksiem/clickdetect
|
|
63
|
+
cd clickdetect
|
|
64
|
+
podman build -t clickdetect .
|
|
65
|
+
podman run --rm -v ./runner.yml:/app/runner.yml -p 8080:8080 clickdetect --api
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Contribution
|
|
69
|
+
|
|
70
|
+
* Like this project
|
|
71
|
+
* Help me to create a sigma converter for clickhouse.
|
|
72
|
+
* Report bugs in the issues
|
|
73
|
+
|
|
74
|
+
## Contact
|
|
75
|
+
|
|
76
|
+
* E-mail: me@souzo.me <vinicius morais>
|
|
77
|
+
* [Linkedin](https://www.linkedin.com/in/vinicius-f-a76ba51b5/)
|
|
78
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
clickdetect.py
|
|
3
|
+
pyproject.toml
|
|
4
|
+
api/detector.py
|
|
5
|
+
api/health.py
|
|
6
|
+
api/rules.py
|
|
7
|
+
clickdetect.egg-info/PKG-INFO
|
|
8
|
+
clickdetect.egg-info/SOURCES.txt
|
|
9
|
+
clickdetect.egg-info/dependency_links.txt
|
|
10
|
+
clickdetect.egg-info/entry_points.txt
|
|
11
|
+
clickdetect.egg-info/requires.txt
|
|
12
|
+
clickdetect.egg-info/top_level.txt
|
|
13
|
+
detector/__init__.py
|
|
14
|
+
detector/config.py
|
|
15
|
+
detector/detector.py
|
|
16
|
+
detector/manager.py
|
|
17
|
+
detector/rules.py
|
|
18
|
+
detector/runner.py
|
|
19
|
+
detector/utils.py
|
|
20
|
+
detector/datasource/__init__.py
|
|
21
|
+
detector/datasource/base.py
|
|
22
|
+
detector/datasource/clickhouse.py
|
|
23
|
+
detector/datasource/elasticsearch.py
|
|
24
|
+
detector/datasource/loki.py
|
|
25
|
+
detector/datasource/postgresql.py
|
|
26
|
+
detector/webhooks/__init__.py
|
|
27
|
+
detector/webhooks/base.py
|
|
28
|
+
detector/webhooks/email.py
|
|
29
|
+
detector/webhooks/generic.py
|
|
30
|
+
detector/webhooks/matrix.py
|
|
31
|
+
detector/webhooks/teams.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uvicorn
|
|
3
|
+
import detector.config as config
|
|
4
|
+
import argparse
|
|
5
|
+
from typing import Any
|
|
6
|
+
from detector.runner import Runner
|
|
7
|
+
from detector.manager import Manager, get_manager_instance
|
|
8
|
+
from logging import getLogger
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from api.detector import router as detector_router
|
|
11
|
+
from api.rules import router as rules_router
|
|
12
|
+
from os.path import exists as f_exists
|
|
13
|
+
|
|
14
|
+
config.logConfig()
|
|
15
|
+
logger = getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
async def load_api(args: Any):
|
|
18
|
+
app = FastAPI(title=config.app_name)
|
|
19
|
+
app.include_router(detector_router)
|
|
20
|
+
app.include_router(rules_router)
|
|
21
|
+
|
|
22
|
+
server_config = uvicorn.Config(app, host='0.0.0.0', port=args.port, log_level='info')
|
|
23
|
+
server = uvicorn.Server(server_config)
|
|
24
|
+
await server.serve()
|
|
25
|
+
|
|
26
|
+
async def load_runner(args: Any) -> Runner | None:
|
|
27
|
+
runner = await Runner(args.runner).init()
|
|
28
|
+
if not runner:
|
|
29
|
+
logger.debug('Runner not loaded')
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
detectors = await runner.get_detectors()
|
|
33
|
+
manager = Manager()
|
|
34
|
+
|
|
35
|
+
if not detectors:
|
|
36
|
+
logger.error('No detector found')
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
for detector in detectors:
|
|
40
|
+
await manager.run_detector(detector)
|
|
41
|
+
|
|
42
|
+
return runner
|
|
43
|
+
|
|
44
|
+
async def loop_run(runner: Runner | None = None):
|
|
45
|
+
try:
|
|
46
|
+
while await config.is_running():
|
|
47
|
+
await asyncio.sleep(1)
|
|
48
|
+
except asyncio.CancelledError:
|
|
49
|
+
logger.warning('received kill event')
|
|
50
|
+
finally:
|
|
51
|
+
await get_manager_instance().shutdown()
|
|
52
|
+
if runner:
|
|
53
|
+
await runner.close()
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
parser = argparse.ArgumentParser(description=f'{config.app_name} is a tool to detect patterns and alerts in clickhouse and others database')
|
|
57
|
+
parser.add_argument('--api', required=False, default=False, action='store_true', help='Enable api, required for clicksiem-backend')
|
|
58
|
+
parser.add_argument('-p', '--port', default=config.default_port, type=int, help=f'specify api port, default: {config.default_port}')
|
|
59
|
+
parser.add_argument('-r', '--runner', default=config.default_runner, type=str, help=f'Runner file containing webhook, datasources, detectors and rules. Default: {config.default_runner}')
|
|
60
|
+
parser.add_argument('--stdin', default=False, type=bool, help='Read file from stdin')
|
|
61
|
+
args = parser.parse_args()
|
|
62
|
+
|
|
63
|
+
tasks = []
|
|
64
|
+
if args.runner:
|
|
65
|
+
if not f_exists(args.runner):
|
|
66
|
+
logger.fatal(f'File {args.runner} does not exists')
|
|
67
|
+
exit(1)
|
|
68
|
+
tasks.append(await load_runner(args))
|
|
69
|
+
if args.api:
|
|
70
|
+
tasks.append(load_api(args))
|
|
71
|
+
if tasks:
|
|
72
|
+
await asyncio.gather(*tasks)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run():
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
run()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from asyncio import Lock
|
|
3
|
+
import colorlog
|
|
4
|
+
|
|
5
|
+
_lock = Lock()
|
|
6
|
+
running = True
|
|
7
|
+
rule_eval_semaphore = 7
|
|
8
|
+
webhook_send_semaphore = 7
|
|
9
|
+
|
|
10
|
+
def logConfig():
|
|
11
|
+
colorlog.basicConfig(
|
|
12
|
+
level=colorlog.DEBUG,
|
|
13
|
+
format="%(asctime)s | %(log_color)s%(levelname)-8s%(reset)s | %(name)s | %(message)s",
|
|
14
|
+
log_colors={
|
|
15
|
+
'DEBUG': 'cyan',
|
|
16
|
+
'INFO': 'green',
|
|
17
|
+
'WARNING': 'yellow',
|
|
18
|
+
'ERROR': 'red',
|
|
19
|
+
'CRITICAL': 'red,bg_white',
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
async def is_running():
|
|
24
|
+
global running
|
|
25
|
+
async with _lock:
|
|
26
|
+
return running
|
|
27
|
+
|
|
28
|
+
async def stop_running():
|
|
29
|
+
global running
|
|
30
|
+
async with _lock:
|
|
31
|
+
running = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
app_name = 'ClickDetector'
|
|
35
|
+
default_runner = 'runner.yml'
|
|
36
|
+
default_port = 8080
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Dict, List, Type
|
|
2
|
+
from detector.datasource.base import BaseDataSource
|
|
3
|
+
from detector.datasource.clickhouse import ClickhouseDataSource
|
|
4
|
+
from detector.datasource.loki import LokiDataSource
|
|
5
|
+
from detector.datasource.elasticsearch import ElasticsearchDataSource
|
|
6
|
+
from detector.datasource.postgresql import PostgreSQLDataSource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
datasources: List[Type[BaseDataSource]] = [
|
|
10
|
+
ClickhouseDataSource,
|
|
11
|
+
LokiDataSource,
|
|
12
|
+
ElasticsearchDataSource,
|
|
13
|
+
PostgreSQLDataSource,
|
|
14
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass()
|
|
6
|
+
class DataSourceQueryResult:
|
|
7
|
+
len: int
|
|
8
|
+
value: Any
|
|
9
|
+
|
|
10
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
11
|
+
return {
|
|
12
|
+
'len': self.len,
|
|
13
|
+
'value': self.value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class BaseDataSource:
|
|
17
|
+
async def connect(self):
|
|
18
|
+
raise NotImplementedError()
|
|
19
|
+
|
|
20
|
+
async def query(self, data: str) -> DataSourceQueryResult | None:
|
|
21
|
+
raise NotImplementedError()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _name(cls) -> str:
|
|
25
|
+
raise NotImplementedError()
|
|
26
|
+
|
|
27
|
+
async def _parse(self, _obj: Any):
|
|
28
|
+
raise NotImplementedError()
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict:
|
|
31
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
from clickhouse_connect import get_async_client
|
|
3
|
+
from clickhouse_connect.driver.asyncclient import AsyncClient
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
from .base import BaseDataSource, DataSourceQueryResult
|
|
7
|
+
|
|
8
|
+
logger = getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class ClickhouseDataSource(BaseDataSource):
|
|
11
|
+
database: str
|
|
12
|
+
host: str
|
|
13
|
+
port: int
|
|
14
|
+
username: str
|
|
15
|
+
password: str
|
|
16
|
+
verify: bool = False
|
|
17
|
+
client: AsyncClient | None = None
|
|
18
|
+
|
|
19
|
+
async def connect(self):
|
|
20
|
+
try:
|
|
21
|
+
self.client = await get_async_client(
|
|
22
|
+
database=self.database,
|
|
23
|
+
host=self.host,
|
|
24
|
+
username=self.username,
|
|
25
|
+
password=self.password,
|
|
26
|
+
port=self.port,
|
|
27
|
+
secure=self.verify
|
|
28
|
+
)
|
|
29
|
+
except Exception as ex:
|
|
30
|
+
logger.error(f'Failed to connect to ClickHouse at {self.host}:{self.port} | {ex}')
|
|
31
|
+
self.client = None
|
|
32
|
+
|
|
33
|
+
async def query(self, data: str) -> DataSourceQueryResult | None:
|
|
34
|
+
if not self.client:
|
|
35
|
+
await self.connect()
|
|
36
|
+
if not self.client:
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
result = await self.client.query(data)
|
|
40
|
+
return DataSourceQueryResult(result.row_count, list(result.named_results()))
|
|
41
|
+
except Exception as ex:
|
|
42
|
+
logger.error(f'Query failed, resetting client | {ex}')
|
|
43
|
+
self.client = None
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def _name(cls) -> str:
|
|
48
|
+
return 'clickhouse'
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> Dict:
|
|
51
|
+
return {
|
|
52
|
+
'database': self.database,
|
|
53
|
+
'host': self.host,
|
|
54
|
+
'port': self.port,
|
|
55
|
+
'username': self.username,
|
|
56
|
+
'password': self.password,
|
|
57
|
+
'verify': self.verify
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async def _parse(self, _obj: Any):
|
|
61
|
+
database = _obj.get('database', 'default')
|
|
62
|
+
host = _obj.get('host')
|
|
63
|
+
port = _obj.get('port')
|
|
64
|
+
username = _obj.get('username')
|
|
65
|
+
password = _obj.get('password')
|
|
66
|
+
verify = _obj.get('verify', False)
|
|
67
|
+
|
|
68
|
+
if not host or not port or not username or not password:
|
|
69
|
+
raise Exception(f'Invalid parameters: {self.to_dict().items()}')
|
|
70
|
+
|
|
71
|
+
self.database = database
|
|
72
|
+
self.host = host
|
|
73
|
+
self.port = port
|
|
74
|
+
self.username = username
|
|
75
|
+
self.password = password
|
|
76
|
+
self.verify = verify
|