Skip to content

Commit b7dfe1d

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat: add VeriSimDB connector and pattern detection
- lib/verisimdb_connector.ex: reads scan results from verisimdb-data - lib/pattern_analyzer.ex: analyzes scans and generates summaries - prolog/pattern_detection.lgt: Logtalk rules for pattern detection Implements Task 4 from verisimdb/SONNET-TASKS.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8feb95c commit b7dfe1d

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

lib/pattern_analyzer.ex

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
3+
defmodule Hypatia.PatternAnalyzer do
4+
@moduledoc """
5+
Analyzes scan results from verisimdb and detects patterns.
6+
"""
7+
8+
alias Hypatia.VerisimdbConnector
9+
10+
require Logger
11+
12+
def analyze_all_scans do
13+
scans = VerisimdbConnector.fetch_all_scans()
14+
15+
Logger.info("Loaded #{length(scans)} scan results")
16+
17+
# Write facts to temp file
18+
facts = Enum.map_join(scans, "\n", &VerisimdbConnector.to_logtalk_facts/1)
19+
facts_file = "/tmp/scan_facts.lgt"
20+
File.write!(facts_file, facts)
21+
22+
Logger.info("Wrote Logtalk facts to #{facts_file}")
23+
24+
# TODO: Integrate with actual Logtalk interpreter
25+
# For now, just return the scans
26+
{:ok, %{
27+
scan_count: length(scans),
28+
facts_file: facts_file,
29+
scans: scans
30+
}}
31+
end
32+
33+
def generate_summary(scans) do
34+
total_weak_points =
35+
scans
36+
|> Enum.map(fn scan ->
37+
Map.get(scan.scan, "weak_points", []) |> length()
38+
end)
39+
|> Enum.sum()
40+
41+
repos_by_severity =
42+
scans
43+
|> Enum.map(fn scan ->
44+
summary = Map.get(scan.scan, "summary", %{})
45+
{scan.repo, summary}
46+
end)
47+
|> Enum.into(%{})
48+
49+
%{
50+
total_repos: length(scans),
51+
total_weak_points: total_weak_points,
52+
repos_by_severity: repos_by_severity
53+
}
54+
end
55+
end

lib/verisimdb_connector.ex

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
3+
defmodule Hypatia.VerisimdbConnector do
4+
@moduledoc """
5+
Reads scan results from verisimdb-data repo and transforms into Logtalk facts.
6+
"""
7+
8+
@verisimdb_data_path "~/Documents/hyperpolymath-repos/verisimdb-data"
9+
10+
def fetch_all_scans do
11+
scans_path = Path.join(expand_path(@verisimdb_data_path), "scans")
12+
13+
case File.ls(scans_path) do
14+
{:ok, files} ->
15+
files
16+
|> Enum.filter(&String.ends_with?(&1, ".json"))
17+
|> Enum.map(&load_scan/1)
18+
|> Enum.reject(&is_nil/1)
19+
20+
{:error, reason} ->
21+
require Logger
22+
Logger.error("Failed to read scans directory: #{inspect(reason)}")
23+
[]
24+
end
25+
end
26+
27+
defp load_scan(filename) do
28+
path = Path.join([expand_path(@verisimdb_data_path), "scans", filename])
29+
repo_name = String.replace(filename, ".json", "")
30+
31+
with {:ok, content} <- File.read(path),
32+
{:ok, data} <- Jason.decode(content) do
33+
%{repo: repo_name, scan: data}
34+
else
35+
{:error, reason} ->
36+
require Logger
37+
Logger.error("Failed to load scan #{filename}: #{inspect(reason)}")
38+
nil
39+
end
40+
end
41+
42+
def to_logtalk_facts(scan_data) do
43+
# Transform weak points into Logtalk facts
44+
weak_points = Map.get(scan_data.scan, "weak_points", [])
45+
46+
weak_points
47+
|> Enum.map(fn wp ->
48+
file = Map.get(wp, "file", "unknown")
49+
category = Map.get(wp, "category", "unknown")
50+
severity = Map.get(wp, "severity", "unknown")
51+
52+
"""
53+
weak_point('#{scan_data.repo}', '#{file}', '#{category}', '#{severity}').
54+
"""
55+
end)
56+
|> Enum.join("\n")
57+
end
58+
59+
defp expand_path(path) do
60+
Path.expand(path)
61+
end
62+
end

prolog/pattern_detection.lgt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
% SPDX-License-Identifier: PMPL-1.0-or-later
2+
3+
% Pattern detection rules for scan results
4+
5+
:- object(pattern_detector).
6+
7+
% Detect widespread unsafe pattern
8+
:- public(widespread_unsafe/2).
9+
widespread_unsafe(Pattern, Repos) :-
10+
findall(Repo, weak_point(Repo, _, Pattern, high), Repos),
11+
length(Repos, Count),
12+
Count >= 3.
13+
14+
% Detect deteriorating repo
15+
:- public(deteriorating/1).
16+
deteriorating(Repo) :-
17+
% TODO: Compare current vs previous scan
18+
% Needs temporal data
19+
fail.
20+
21+
% Detect critical weak points in a repo
22+
:- public(critical_weak_points/2).
23+
critical_weak_points(Repo, Count) :-
24+
findall(1, weak_point(Repo, _, _, critical), List),
25+
length(List, Count),
26+
Count > 0.
27+
28+
% Find all repos with a specific weakness type
29+
:- public(repos_with_weakness/2).
30+
repos_with_weakness(WeaknessType, Repos) :-
31+
findall(Repo, weak_point(Repo, _, WeaknessType, _), RepoList),
32+
list::unique(RepoList, Repos).
33+
34+
% Calculate risk score for a repo
35+
:- public(repo_risk_score/2).
36+
repo_risk_score(Repo, Score) :-
37+
findall(S, (weak_point(Repo, _, _, Severity), severity_score(Severity, S)), Scores),
38+
list::sum(Scores, Score).
39+
40+
% Helper: severity to numeric score
41+
severity_score(critical, 10).
42+
severity_score(high, 5).
43+
severity_score(medium, 2).
44+
severity_score(low, 1).
45+
46+
:- end_object.

0 commit comments

Comments
 (0)