chronicle-devkit 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.
- chronicle/__init__.py +1 -0
- chronicle/ai/__init__.py +0 -0
- chronicle/ai/base.py +7 -0
- chronicle/ai/factory.py +23 -0
- chronicle/ai/google_ai.py +14 -0
- chronicle/ai/openai.py +14 -0
- chronicle/analysis/__init__.py +0 -0
- chronicle/analysis/analyzers/__init__.py +0 -0
- chronicle/analysis/analyzers/base.py +15 -0
- chronicle/analysis/analyzers/django_migrations.py +74 -0
- chronicle/analysis/analyzers/git.py +37 -0
- chronicle/analysis/context.py +12 -0
- chronicle/analysis/engine.py +18 -0
- chronicle/cli/__init__.py +1 -0
- chronicle/cli/commands/__init__.py +0 -0
- chronicle/cli/commands/analyze.py +73 -0
- chronicle/cli/commands/config.py +79 -0
- chronicle/cli/commands/init.py +47 -0
- chronicle/cli/commands/interpret.py +48 -0
- chronicle/cli/commands/scan.py +87 -0
- chronicle/cli/commands/show.py +106 -0
- chronicle/cli/commands/status.py +26 -0
- chronicle/cli/commands/version.py +6 -0
- chronicle/cli/main.py +30 -0
- chronicle/config/__init__.py +0 -0
- chronicle/config/credentials.py +17 -0
- chronicle/config/loader.py +21 -0
- chronicle/config/manager.py +52 -0
- chronicle/integrations/__init__.py +0 -0
- chronicle/integrations/git.py +16 -0
- chronicle/interpretation/__init__.py +0 -0
- chronicle/interpretation/context.py +8 -0
- chronicle/interpretation/context_builder.py +147 -0
- chronicle/interpretation/interpreter.py +12 -0
- chronicle/interpretation/prompts.py +51 -0
- chronicle/project/__init__.py +0 -0
- chronicle/project/discovery.py +9 -0
- chronicle/project/initializer.py +45 -0
- chronicle/project/status.py +25 -0
- chronicle/scanning/__init__.py +0 -0
- chronicle/scanning/context.py +10 -0
- chronicle/scanning/engine.py +16 -0
- chronicle/scanning/scanners/__init__.py +0 -0
- chronicle/scanning/scanners/base.py +17 -0
- chronicle/scanning/scanners/django_migrations.py +156 -0
- chronicle/scanning/scanners/django_model.py +12 -0
- chronicle/scanning/scanners/git.py +94 -0
- chronicle/scanning/scanners/git_models.py +6 -0
- chronicle/storage/__init__.py +0 -0
- chronicle/storage/analysis_state.py +44 -0
- chronicle/storage/database.py +9 -0
- chronicle/storage/findings.py +53 -0
- chronicle/storage/models.py +20 -0
- chronicle/storage/observations.py +93 -0
- chronicle/storage/scan_state.py +44 -0
- chronicle/storage/schema.py +47 -0
- chronicle_devkit-0.1.0.dist-info/METADATA +233 -0
- chronicle_devkit-0.1.0.dist-info/RECORD +61 -0
- chronicle_devkit-0.1.0.dist-info/WHEEL +5 -0
- chronicle_devkit-0.1.0.dist-info/entry_points.txt +2 -0
- chronicle_devkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import ast
|
|
3
|
+
from datetime import datetime,timezone
|
|
4
|
+
from .base import Scanner
|
|
5
|
+
from chronicle.scanning.context import ScanContext
|
|
6
|
+
from chronicle.storage.observations import Observation
|
|
7
|
+
from .django_model import MigrationOperation
|
|
8
|
+
|
|
9
|
+
class DjangoMigrationScanner(Scanner):
|
|
10
|
+
def __init__(self, project_root: Path) -> None:
|
|
11
|
+
super().__init__(project_root)
|
|
12
|
+
|
|
13
|
+
def scan(self, contexts: list[ScanContext]) -> list[Observation]:
|
|
14
|
+
observation = []
|
|
15
|
+
last_migration = None
|
|
16
|
+
last_migration_name = None
|
|
17
|
+
last_app = None
|
|
18
|
+
for context in contexts:
|
|
19
|
+
last_migration = context.get_state("django_migrations",".last_migration")
|
|
20
|
+
if last_migration:
|
|
21
|
+
break
|
|
22
|
+
for migration_file in self._find_migrations():
|
|
23
|
+
migration_name = migration_file.stem
|
|
24
|
+
app_label = migration_file.parent.parent.name
|
|
25
|
+
if last_migration:
|
|
26
|
+
last_app , last_migration_name = last_migration.split(":",1,)
|
|
27
|
+
if app_label == last_app:
|
|
28
|
+
if self._migration_num(migration_name) <= self._migration_num(last_migration_name):
|
|
29
|
+
continue
|
|
30
|
+
observation.append(
|
|
31
|
+
self._parse_migration(
|
|
32
|
+
migration_file
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return observation
|
|
37
|
+
|
|
38
|
+
def _find_migrations(self) -> list[Path]:
|
|
39
|
+
migration_files = []
|
|
40
|
+
for path in self.project_root.rglob(
|
|
41
|
+
"migrations/*.py"
|
|
42
|
+
):
|
|
43
|
+
if self._is_ignored(path):
|
|
44
|
+
continue
|
|
45
|
+
migration_files.append(path)
|
|
46
|
+
return migration_files
|
|
47
|
+
|
|
48
|
+
def _is_ignored(self, path:Path) -> bool:
|
|
49
|
+
IGNORED_DIRECTORIES = {
|
|
50
|
+
".git",
|
|
51
|
+
".venv",
|
|
52
|
+
"venv",
|
|
53
|
+
"env",
|
|
54
|
+
"node_modules",
|
|
55
|
+
"__pycache__",
|
|
56
|
+
}
|
|
57
|
+
return any(part in IGNORED_DIRECTORIES for part in path.parts)
|
|
58
|
+
|
|
59
|
+
def _parse_migration(self,migration_file:Path) -> Observation:
|
|
60
|
+
source = migration_file.read_text(encoding="utf-8")
|
|
61
|
+
migration_name = migration_file.stem
|
|
62
|
+
app_label = migration_file.parent.parent.name
|
|
63
|
+
tree = ast.parse(source)
|
|
64
|
+
dependencies = []
|
|
65
|
+
parsed_operations = []
|
|
66
|
+
|
|
67
|
+
for node in ast.walk(tree):
|
|
68
|
+
if isinstance(node,ast.ClassDef):
|
|
69
|
+
if node.name == "Migration":
|
|
70
|
+
migration_class = node
|
|
71
|
+
for statement in migration_class.body:
|
|
72
|
+
if isinstance(statement,ast.Assign):
|
|
73
|
+
target = statement.targets[0]
|
|
74
|
+
if isinstance(target, ast.Name):
|
|
75
|
+
if target.id == "dependencies":
|
|
76
|
+
dependencies = ast.literal_eval(statement.value)
|
|
77
|
+
elif target.id == "operations":
|
|
78
|
+
if not isinstance(
|
|
79
|
+
statement.value,
|
|
80
|
+
ast.List,
|
|
81
|
+
):
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
parsed_operations.extend(
|
|
85
|
+
self.get_operations(
|
|
86
|
+
statement.value
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return Observation(
|
|
91
|
+
source="django",
|
|
92
|
+
type="migration",
|
|
93
|
+
external_id=f"{app_label}:{migration_name}",
|
|
94
|
+
timestamp=datetime.now(timezone.utc),
|
|
95
|
+
data={
|
|
96
|
+
"app": app_label,
|
|
97
|
+
"name": migration_name,
|
|
98
|
+
"dependencies": dependencies,
|
|
99
|
+
"operations": parsed_operations,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_operations(
|
|
104
|
+
self,
|
|
105
|
+
operations_node: ast.List,
|
|
106
|
+
) -> list[MigrationOperation]:
|
|
107
|
+
|
|
108
|
+
operations = []
|
|
109
|
+
|
|
110
|
+
for operation_node in operations_node.elts:
|
|
111
|
+
|
|
112
|
+
if not isinstance(
|
|
113
|
+
operation_node,
|
|
114
|
+
ast.Call,
|
|
115
|
+
):
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if not isinstance(
|
|
119
|
+
operation_node.func,
|
|
120
|
+
ast.Attribute,
|
|
121
|
+
):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
operation_name = operation_node.func.attr
|
|
125
|
+
|
|
126
|
+
model_name = None
|
|
127
|
+
field_name = None
|
|
128
|
+
|
|
129
|
+
for keyword in operation_node.keywords:
|
|
130
|
+
|
|
131
|
+
if keyword.arg == "model_name":
|
|
132
|
+
|
|
133
|
+
model_name = ast.literal_eval(
|
|
134
|
+
keyword.value
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
elif keyword.arg == "name":
|
|
138
|
+
|
|
139
|
+
field_name = ast.literal_eval(
|
|
140
|
+
keyword.value
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
operations.append(
|
|
144
|
+
MigrationOperation(
|
|
145
|
+
operation=operation_name,
|
|
146
|
+
details={
|
|
147
|
+
"model": model_name,
|
|
148
|
+
"field": field_name,
|
|
149
|
+
},
|
|
150
|
+
).to_dict()
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return operations
|
|
154
|
+
|
|
155
|
+
def _migration_num(self,migration_name:str) -> int:
|
|
156
|
+
return int(migration_name.split("_",1)[0])
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from chronicle.integrations.git import GitIntegration
|
|
5
|
+
from ...storage.models import Observation
|
|
6
|
+
from .git_models import FileChange
|
|
7
|
+
from .base import Scanner
|
|
8
|
+
from chronicle.scanning.context import ScanContext
|
|
9
|
+
|
|
10
|
+
class GitScanner(Scanner):
|
|
11
|
+
"""Scanner for Git repo history."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_root: Path) -> None:
|
|
14
|
+
super().__init__(project_root)
|
|
15
|
+
self.git = GitIntegration(project_root)
|
|
16
|
+
|
|
17
|
+
def scan(self,contexts:list[ScanContext]) -> list[Observation]:
|
|
18
|
+
for context in contexts:
|
|
19
|
+
last_commit = context.get_state("git","last_commit")
|
|
20
|
+
if last_commit:
|
|
21
|
+
break
|
|
22
|
+
try:
|
|
23
|
+
if last_commit:
|
|
24
|
+
output = self.git.run(
|
|
25
|
+
"log",
|
|
26
|
+
f"{last_commit}..HEAD",
|
|
27
|
+
"--format=%H|%P|%aI|%an|%s",
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
output = self.git.run(
|
|
31
|
+
"log",
|
|
32
|
+
"--format=%H|%P|%aI|%an|%s",
|
|
33
|
+
)
|
|
34
|
+
except subprocess.CalledProcessError:
|
|
35
|
+
return []
|
|
36
|
+
observation:list[Observation] = []
|
|
37
|
+
for line in output.splitlines():
|
|
38
|
+
line = line.strip()
|
|
39
|
+
if not line:
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
commit_hash, parents, timestamp, author, msg = line.split("|",4)
|
|
43
|
+
|
|
44
|
+
parents_hash = parents.split() if parents else []
|
|
45
|
+
|
|
46
|
+
changes = self.get_changed_file(commit_hash=commit_hash)
|
|
47
|
+
|
|
48
|
+
observation.append(Observation(
|
|
49
|
+
source="git",
|
|
50
|
+
type="commit",
|
|
51
|
+
external_id=commit_hash,
|
|
52
|
+
timestamp=datetime.fromisoformat(timestamp),
|
|
53
|
+
data={
|
|
54
|
+
"hash":commit_hash,
|
|
55
|
+
"message":msg,
|
|
56
|
+
"author":author,
|
|
57
|
+
"parents":parents_hash,
|
|
58
|
+
"changes": [
|
|
59
|
+
{
|
|
60
|
+
"path": change.path,
|
|
61
|
+
"status": change.status,
|
|
62
|
+
}
|
|
63
|
+
for change in changes
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
return observation
|
|
69
|
+
|
|
70
|
+
def get_changed_file(self,commit_hash:str) -> list[FileChange]:
|
|
71
|
+
changes_observed = self.git.run(
|
|
72
|
+
"diff-tree",
|
|
73
|
+
"--no-commit-id",
|
|
74
|
+
"--name-status",
|
|
75
|
+
"-r",
|
|
76
|
+
commit_hash,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
changes = []
|
|
80
|
+
for line in changes_observed.splitlines():
|
|
81
|
+
line = line.strip()
|
|
82
|
+
if not line:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
status, path = line.split("\t", 1)
|
|
86
|
+
|
|
87
|
+
changes.append(
|
|
88
|
+
FileChange(
|
|
89
|
+
path=path,
|
|
90
|
+
status=status,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return changes
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
class AnalysisStateRepo:
|
|
5
|
+
def __init__(self,conn:sqlite3.Connection) -> None:
|
|
6
|
+
self.conn = conn
|
|
7
|
+
|
|
8
|
+
def get(self,key:str,analyzer:str) -> str | None:
|
|
9
|
+
row = self.conn.execute(
|
|
10
|
+
"""
|
|
11
|
+
SELECT value FROM analysis_state WHERE key=? AND analyzer=?;
|
|
12
|
+
""",
|
|
13
|
+
(key,analyzer),
|
|
14
|
+
).fetchone()
|
|
15
|
+
|
|
16
|
+
if row is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
return row["value"]
|
|
20
|
+
|
|
21
|
+
def set(self,key:str,analyzer:str,value:str) -> None:
|
|
22
|
+
self.conn.execute(
|
|
23
|
+
"""
|
|
24
|
+
INSERT INTO analysis_state(
|
|
25
|
+
analyzer,
|
|
26
|
+
key,
|
|
27
|
+
value,
|
|
28
|
+
updated_at
|
|
29
|
+
)
|
|
30
|
+
VALUES (?,?,?,?)
|
|
31
|
+
|
|
32
|
+
ON CONFLICT(analyzer,key)
|
|
33
|
+
DO UPDATE SET
|
|
34
|
+
value = excluded.value,
|
|
35
|
+
updated_at = excluded.updated_at
|
|
36
|
+
""",
|
|
37
|
+
(
|
|
38
|
+
analyzer,
|
|
39
|
+
key,
|
|
40
|
+
value,
|
|
41
|
+
datetime.now(timezone.utc).isoformat(),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def connect(database_path:Path) -> sqlite3.Connection:
|
|
5
|
+
connection = sqlite3.connect(database_path)
|
|
6
|
+
connection.row_factory = sqlite3.Row
|
|
7
|
+
cursor = connection.cursor()
|
|
8
|
+
cursor.execute("PRAGMA foreign_keys = ON;")
|
|
9
|
+
return connection
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from chronicle.storage.models import Findings
|
|
4
|
+
|
|
5
|
+
class FindingsRepo:
|
|
6
|
+
def __init__(self,conn:sqlite3.Connection) -> None:
|
|
7
|
+
self.conn = conn
|
|
8
|
+
|
|
9
|
+
def save(self, finding:Findings) -> bool:
|
|
10
|
+
cursor = self.conn.execute(
|
|
11
|
+
"""
|
|
12
|
+
INSERT INTO
|
|
13
|
+
findings(
|
|
14
|
+
analyzer,
|
|
15
|
+
severity,
|
|
16
|
+
title,
|
|
17
|
+
message,
|
|
18
|
+
observation_id,
|
|
19
|
+
data
|
|
20
|
+
) VALUES (?,?,?,?,?,?)
|
|
21
|
+
""",
|
|
22
|
+
(
|
|
23
|
+
finding.analyzer,
|
|
24
|
+
finding.severity,
|
|
25
|
+
finding.title,
|
|
26
|
+
finding.message,
|
|
27
|
+
finding.observation_id,
|
|
28
|
+
json.dumps(finding.data),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return cursor.rowcount == 1
|
|
33
|
+
|
|
34
|
+
def list_all(self) -> list[Findings]:
|
|
35
|
+
findings = []
|
|
36
|
+
|
|
37
|
+
rows = self.conn.execute(
|
|
38
|
+
"""
|
|
39
|
+
SELECT * FROM findings ORDER BY observation_id;
|
|
40
|
+
"""
|
|
41
|
+
).fetchall()
|
|
42
|
+
|
|
43
|
+
for row in rows:
|
|
44
|
+
findings.append(Findings(
|
|
45
|
+
analyzer=row["analyzer"],
|
|
46
|
+
severity=row["severity"],
|
|
47
|
+
title=row["title"],
|
|
48
|
+
message=row["message"],
|
|
49
|
+
observation_id=row["observation_id"],
|
|
50
|
+
data=json.loads(row["data"]),
|
|
51
|
+
))
|
|
52
|
+
|
|
53
|
+
return findings
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass,field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Observation:
|
|
6
|
+
source:str
|
|
7
|
+
type:str
|
|
8
|
+
external_id:str
|
|
9
|
+
timestamp:datetime
|
|
10
|
+
data:dict
|
|
11
|
+
id:int | None = None
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Findings:
|
|
15
|
+
analyzer: str
|
|
16
|
+
severity: str
|
|
17
|
+
title: str
|
|
18
|
+
message: str
|
|
19
|
+
observation_id: int
|
|
20
|
+
data: dict = field(default_factory=dict)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from chronicle.storage.models import Observation
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
class ObservationRepo:
|
|
7
|
+
def __init__(self, connection:sqlite3.Connection) -> None:
|
|
8
|
+
self.conn = connection
|
|
9
|
+
|
|
10
|
+
def save(self,observation:Observation)->bool:
|
|
11
|
+
cursor = self.conn.execute(
|
|
12
|
+
"""
|
|
13
|
+
INSERT OR IGNORE INTO observations(
|
|
14
|
+
source,
|
|
15
|
+
type,
|
|
16
|
+
external_id,
|
|
17
|
+
timestamp,
|
|
18
|
+
data
|
|
19
|
+
) VALUES (?,?,?,?,?)
|
|
20
|
+
""",
|
|
21
|
+
(
|
|
22
|
+
observation.source,
|
|
23
|
+
observation.type,
|
|
24
|
+
observation.external_id,
|
|
25
|
+
observation.timestamp.isoformat(),
|
|
26
|
+
json.dumps(observation.data),
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return cursor.rowcount == 1
|
|
31
|
+
|
|
32
|
+
def list_all(self,id:int | None = None) -> list[Observation]:
|
|
33
|
+
observations = []
|
|
34
|
+
if id:
|
|
35
|
+
row = self.conn.execute(
|
|
36
|
+
"""
|
|
37
|
+
SELECT
|
|
38
|
+
id,
|
|
39
|
+
source,
|
|
40
|
+
type,
|
|
41
|
+
external_id,
|
|
42
|
+
timestamp,
|
|
43
|
+
data
|
|
44
|
+
FROM observations
|
|
45
|
+
WHERE id = ?;
|
|
46
|
+
""",
|
|
47
|
+
(id,),
|
|
48
|
+
).fetchone()
|
|
49
|
+
|
|
50
|
+
if row is None:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
observations.append(
|
|
54
|
+
Observation(
|
|
55
|
+
source=row["source"],
|
|
56
|
+
type=row["type"],
|
|
57
|
+
external_id=row["external_id"],
|
|
58
|
+
timestamp=datetime.fromisoformat(
|
|
59
|
+
row["timestamp"]
|
|
60
|
+
),
|
|
61
|
+
data=json.loads(row["data"]),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
rows = self.conn.execute(
|
|
66
|
+
"""
|
|
67
|
+
SELECT
|
|
68
|
+
id,
|
|
69
|
+
source,
|
|
70
|
+
type,
|
|
71
|
+
external_id,
|
|
72
|
+
timestamp,
|
|
73
|
+
data
|
|
74
|
+
FROM observations
|
|
75
|
+
ORDER BY id
|
|
76
|
+
"""
|
|
77
|
+
).fetchall()
|
|
78
|
+
|
|
79
|
+
for row in rows:
|
|
80
|
+
observations.append(
|
|
81
|
+
Observation(
|
|
82
|
+
id=row["id"],
|
|
83
|
+
source=row["source"],
|
|
84
|
+
type=row["type"],
|
|
85
|
+
external_id=row["external_id"],
|
|
86
|
+
timestamp=datetime.fromisoformat(
|
|
87
|
+
row["timestamp"]
|
|
88
|
+
),
|
|
89
|
+
data=json.loads(row["data"]),
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return observations
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
class ScanStateRepo:
|
|
5
|
+
def __init__(self,conn:sqlite3.Connection) -> None:
|
|
6
|
+
self.conn = conn
|
|
7
|
+
|
|
8
|
+
def get(self,key:str,scanner:str) -> str | None:
|
|
9
|
+
row = self.conn.execute(
|
|
10
|
+
"""
|
|
11
|
+
SELECT VALUE FROM scan_state WHERE key=? AND scanner=?;
|
|
12
|
+
""",
|
|
13
|
+
(key,scanner),
|
|
14
|
+
).fetchone()
|
|
15
|
+
|
|
16
|
+
if row is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
return row["value"]
|
|
20
|
+
|
|
21
|
+
def set(self,key:str,scanner:str,value:str) -> None:
|
|
22
|
+
self.conn.execute(
|
|
23
|
+
"""
|
|
24
|
+
INSERT INTO scan_state(
|
|
25
|
+
scanner,
|
|
26
|
+
key,
|
|
27
|
+
value,
|
|
28
|
+
updated_at
|
|
29
|
+
)
|
|
30
|
+
VALUES (?,?,?,?)
|
|
31
|
+
|
|
32
|
+
ON CONFLICT(scanner,key)
|
|
33
|
+
DO UPDATE SET
|
|
34
|
+
value = excluded.value,
|
|
35
|
+
updated_at = excluded.updated_at
|
|
36
|
+
""",
|
|
37
|
+
(
|
|
38
|
+
scanner,
|
|
39
|
+
key,
|
|
40
|
+
value,
|
|
41
|
+
datetime.now(timezone.utc).isoformat(),
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
|
|
3
|
+
SCHEMA = """
|
|
4
|
+
CREATE TABLE IF NOT EXISTS observations(
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
source TEXT NOT NULL,
|
|
7
|
+
type TEXT NOT NULL,
|
|
8
|
+
external_id TEXT NOT NULL,
|
|
9
|
+
timestamp TEXT NOT NULL,
|
|
10
|
+
data TEXT NOT NULL,
|
|
11
|
+
UNIQUE(source,type,external_id)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS scan_state(
|
|
15
|
+
scanner TEXT NOT NULL,
|
|
16
|
+
key TEXT NOT NULL,
|
|
17
|
+
value TEXT NOT NULL,
|
|
18
|
+
updated_at TEXT NOT NULL,
|
|
19
|
+
PRIMARY KEY(scanner , key)
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
CREATE TABLE IF NOT EXISTS analysis_state(
|
|
23
|
+
analyzer TEXT NOT NULL,
|
|
24
|
+
key TEXT NOT NULL,
|
|
25
|
+
value TEXT NOT NULL,
|
|
26
|
+
updated_at TEXT NOT NULL,
|
|
27
|
+
PRIMARY KEY(analyzer , key)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS findings(
|
|
31
|
+
analyzer TEXT NOT NULL,
|
|
32
|
+
severity TEXT NOT NULL,
|
|
33
|
+
title TEXT NOT NULL,
|
|
34
|
+
message TEXT NOT NULL,
|
|
35
|
+
observation_id INTEGER,
|
|
36
|
+
data TEXT NOT NULL,
|
|
37
|
+
UNIQUE(analyzer,observation_id,data),
|
|
38
|
+
FOREIGN KEY (observation_id)
|
|
39
|
+
REFERENCES observations (id)
|
|
40
|
+
ON DELETE CASCADE
|
|
41
|
+
ON UPDATE CASCADE
|
|
42
|
+
);
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def init_schema(connection:sqlite3.Connection) -> None:
|
|
46
|
+
connection.executescript(SCHEMA)
|
|
47
|
+
connection.commit()
|