Skip to content

Commit 3035f39

Browse files
committed
tweak: add route for fetching Delphi issue type schema, abstract Labrinth away from issue types
1 parent 8a6cbe1 commit 3035f39

File tree

3 files changed

+78
-84
lines changed

3 files changed

+78
-84
lines changed

apps/labrinth/migrations/20250810155316_delphi-reports.sql

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,6 @@ CREATE TYPE delphi_report_severity AS ENUM ('low', 'medium', 'high', 'severe');
22

33
CREATE TYPE delphi_report_issue_status AS ENUM ('pending', 'approved', 'rejected');
44

5-
CREATE TYPE delphi_report_issue_type AS ENUM (
6-
'reflection_indirection',
7-
'xor_obfuscation',
8-
'included_libraries',
9-
'suspicious_binaries',
10-
'corrupt_classes',
11-
'suspicious_classes',
12-
'url_usage',
13-
'classloader_usage',
14-
'processbuilder_usage',
15-
'runtime_exec_usage',
16-
'jni_usage',
17-
'main_method',
18-
'native_loading',
19-
'malformed_jar',
20-
'nested_jar_too_deep',
21-
'failed_decompilation',
22-
'analysis_failure',
23-
'malware_easyforme',
24-
'malware_simplyloader',
25-
'unknown'
26-
);
27-
285
-- A Delphi analysis report for a project version
296
CREATE TABLE delphi_reports (
307
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
@@ -46,7 +23,7 @@ CREATE TABLE delphi_report_issues (
4623
report_id BIGINT NOT NULL REFERENCES delphi_reports (id)
4724
ON DELETE CASCADE
4825
ON UPDATE CASCADE,
49-
issue_type DELPHI_REPORT_ISSUE_TYPE NOT NULL,
26+
issue_type TEXT NOT NULL,
5027
status DELPHI_REPORT_ISSUE_STATUS NOT NULL,
5128
UNIQUE (report_id, issue_type)
5229
);

apps/labrinth/src/database/models/delphi_report_item.rs

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub enum DelphiReportSeverity {
7070
pub struct DBDelphiReportIssue {
7171
pub id: DelphiReportIssueId,
7272
pub report_id: DelphiReportId,
73-
pub issue_type: DelphiReportIssueType,
73+
pub issue_type: String,
7474
pub status: DelphiReportIssueStatus,
7575
}
7676

@@ -141,7 +141,7 @@ impl DBDelphiReportIssue {
141141
RETURNING id
142142
",
143143
self.report_id as DelphiReportId,
144-
self.issue_type as DelphiReportIssueType,
144+
self.issue_type,
145145
self.status as DelphiReportIssueStatus,
146146
)
147147
.fetch_one(&mut **transaction)
@@ -150,7 +150,7 @@ impl DBDelphiReportIssue {
150150
}
151151

152152
pub async fn find_all_by(
153-
ty: Option<DelphiReportIssueType>,
153+
ty: Option<String>,
154154
status: Option<DelphiReportIssueStatus>,
155155
order_by: Option<DelphiReportListOrder>,
156156
count: Option<u16>,
@@ -161,7 +161,7 @@ impl DBDelphiReportIssue {
161161
r#"
162162
SELECT
163163
delphi_report_issues.id AS "id", report_id,
164-
issue_type AS "issue_type: DelphiReportIssueType",
164+
issue_type,
165165
delphi_report_issues.status AS "status: DelphiReportIssueStatus",
166166
167167
file_id, delphi_version, artifact_url, created, severity AS "severity: DelphiReportSeverity",
@@ -187,7 +187,7 @@ impl DBDelphiReportIssue {
187187
OFFSET $5
188188
LIMIT $4
189189
"#,
190-
ty as Option<DelphiReportIssueType>,
190+
ty,
191191
status as Option<DelphiReportIssueStatus>,
192192
order_by.map(|order_by| order_by.to_string()),
193193
count.map(|count| count as i64),
@@ -221,50 +221,6 @@ impl DBDelphiReportIssue {
221221
}
222222
}
223223

224-
/// A type of issue found by Delphi for an artifact.
225-
#[derive(
226-
Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash, sqlx::Type,
227-
)]
228-
#[serde(rename_all = "snake_case")]
229-
#[sqlx(type_name = "delphi_report_issue_type", rename_all = "snake_case")]
230-
pub enum DelphiReportIssueType {
231-
ReflectionIndirection,
232-
XorObfuscation,
233-
IncludedLibraries,
234-
SuspiciousBinaries,
235-
CorruptClasses,
236-
SuspiciousClasses,
237-
238-
UrlUsage,
239-
ClassloaderUsage,
240-
ProcessbuilderUsage,
241-
RuntimeExecUsage,
242-
#[serde(rename = "jni_usage")]
243-
#[sqlx(rename = "jni_usage")]
244-
JNIUsage,
245-
246-
MainMethod,
247-
NativeLoading,
248-
249-
MalformedJar,
250-
NestedJarTooDeep,
251-
FailedDecompilation,
252-
AnalysisFailure,
253-
254-
MalwareEasyforme,
255-
MalwareSimplyloader,
256-
257-
/// An issue reported by Delphi but not known by labrinth yet.
258-
#[serde(other)]
259-
Unknown,
260-
}
261-
262-
impl Display for DelphiReportIssueType {
263-
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
264-
self.serialize(f)
265-
}
266-
}
267-
268224
/// A Java class affected by a Delphi report issue. Every affected
269225
/// Java class belongs to a specific issue, and an issue can have zero,
270226
/// one, or more affected classes. (Some issues may be artifact-wide,

apps/labrinth/src/routes/internal/delphi.rs

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
use std::{collections::HashMap, fmt::Write, sync::LazyLock};
1+
use std::{collections::HashMap, fmt::Write, sync::LazyLock, time::Instant};
22

33
use actix_web::{HttpRequest, HttpResponse, get, post, put, web};
44
use chrono::{DateTime, Utc};
5+
use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
56
use serde::Deserialize;
67
use sqlx::PgPool;
8+
use tokio::sync::Mutex;
79
use tracing::info;
810

911
use crate::{
@@ -15,9 +17,8 @@ use crate::{
1517
delphi_report_item::{
1618
DBDelphiReport, DBDelphiReportIssue,
1719
DBDelphiReportIssueJavaClass, DecompiledJavaClassSource,
18-
DelphiReportIssueStatus, DelphiReportIssueType,
19-
DelphiReportListOrder, DelphiReportSeverity,
20-
InternalJavaClassName,
20+
DelphiReportIssueStatus, DelphiReportListOrder,
21+
DelphiReportSeverity, InternalJavaClassName,
2122
},
2223
},
2324
redis::RedisPool,
@@ -38,10 +39,26 @@ pub fn config(cfg: &mut web::ServiceConfig) {
3839
.service(_run)
3940
.service(version)
4041
.service(issues)
41-
.service(update_issue),
42+
.service(update_issue)
43+
.service(issue_type_schema),
4244
);
4345
}
4446

47+
static DELPHI_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
48+
reqwest::Client::builder()
49+
.default_headers({
50+
HeaderMap::from_iter([(
51+
USER_AGENT,
52+
HeaderValue::from_static(concat!(
53+
"Labrinth/",
54+
env!("COMPILATION_DATE")
55+
)),
56+
)])
57+
})
58+
.build()
59+
.unwrap()
60+
});
61+
4562
#[derive(Deserialize)]
4663
struct DelphiReport {
4764
pub url: String,
@@ -53,7 +70,7 @@ struct DelphiReport {
5370
/// Delphi version that generated this report.
5471
pub delphi_version: i32,
5572
pub issues: HashMap<
56-
DelphiReportIssueType,
73+
String,
5774
HashMap<InternalJavaClassName, Option<DecompiledJavaClassSource>>,
5875
>,
5976
pub severity: DelphiReportSeverity,
@@ -169,9 +186,6 @@ pub async fn run(
169186
.fetch_one(exec)
170187
.await?;
171188

172-
static DELPHI_CLIENT: LazyLock<reqwest::Client> =
173-
LazyLock::new(reqwest::Client::new);
174-
175189
tracing::debug!(
176190
"Running Delphi for project {}, version {}, file {}",
177191
file_data.project_id.0,
@@ -241,7 +255,7 @@ async fn version(
241255
#[derive(Deserialize)]
242256
struct DelphiIssuesSearchOptions {
243257
#[serde(rename = "type")]
244-
ty: Option<DelphiReportIssueType>,
258+
ty: Option<String>,
245259
status: Option<DelphiReportIssueStatus>,
246260
order_by: Option<DelphiReportListOrder>,
247261
count: Option<u16>,
@@ -254,7 +268,7 @@ async fn issues(
254268
pool: web::Data<PgPool>,
255269
redis: web::Data<RedisPool>,
256270
session_queue: web::Data<AuthQueue>,
257-
search_options: web::Query<DelphiIssuesSearchOptions>,
271+
web::Query(search_options): web::Query<DelphiIssuesSearchOptions>,
258272
) -> Result<HttpResponse, ApiError> {
259273
check_is_moderator_from_headers(
260274
&req,
@@ -324,3 +338,50 @@ async fn update_issue(
324338
Ok(HttpResponse::Created().finish())
325339
}
326340
}
341+
342+
#[get("issue_type/schema")]
343+
async fn issue_type_schema(
344+
req: HttpRequest,
345+
pool: web::Data<PgPool>,
346+
redis: web::Data<RedisPool>,
347+
session_queue: web::Data<AuthQueue>,
348+
) -> Result<HttpResponse, ApiError> {
349+
check_is_moderator_from_headers(
350+
&req,
351+
&**pool,
352+
&redis,
353+
&session_queue,
354+
Scopes::PROJECT_READ,
355+
)
356+
.await?;
357+
358+
// This route is expected to be called often by the frontend, and Delphi is not necessarily
359+
// built to scale beyond malware analysis, so cache the result of its quasi-constant-valued
360+
// schema route to alleviate the load on it
361+
362+
static CACHED_ISSUE_TYPE_SCHEMA: Mutex<
363+
Option<(serde_json::Map<String, serde_json::Value>, Instant)>,
364+
> = Mutex::const_new(None);
365+
366+
match &mut *CACHED_ISSUE_TYPE_SCHEMA.lock().await {
367+
Some((schema, last_fetch)) if last_fetch.elapsed().as_secs() < 60 => {
368+
Ok(HttpResponse::Ok().json(schema))
369+
}
370+
cache_entry => Ok(HttpResponse::Ok().json(
371+
&cache_entry
372+
.insert((
373+
DELPHI_CLIENT
374+
.get(format!("{}/schema", dotenvy::var("DELPHI_URL")?))
375+
.send()
376+
.await
377+
.and_then(|res| res.error_for_status())
378+
.map_err(ApiError::Delphi)?
379+
.json::<serde_json::Map<String, serde_json::Value>>()
380+
.await
381+
.map_err(ApiError::Delphi)?,
382+
Instant::now(),
383+
))
384+
.0,
385+
)),
386+
}
387+
}

0 commit comments

Comments
 (0)