adjust schema

This commit is contained in:
aecsocket
2026-08-02 17:05:41 +01:00
parent 5ead1499cd
commit 4c8abfb78d
31 changed files with 577 additions and 275 deletions
Generated
-9
View File
@@ -2909,15 +2909,6 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
[[package]]
name = "delphi-cel-experiment"
version = "0.1.0"
dependencies = [
"cel",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
-1
View File
@@ -8,7 +8,6 @@ members = [
"packages/app-lib", "packages/app-lib",
"packages/ariadne", "packages/ariadne",
"packages/daedalus", "packages/daedalus",
"packages/delphi-cel-experiment",
"packages/labrinth-derive", "packages/labrinth-derive",
"packages/modrinth-content-management", "packages/modrinth-content-management",
"packages/modrinth-log", "packages/modrinth-log",
@@ -461,10 +461,16 @@ const TEST_INPUTS: Labrinth.TechReview.Internal.RuleInput[] = [
sha512: 'fedcba9876543210', sha512: 'fedcba9876543210',
}, },
}, },
scope: { project: {
project_id: 'example-project', id: 'example-project',
version_id: 'example-version', types: ['mod'],
file_id: 'example-file', },
version: {
id: 'example-version',
loaders: ['fabric'],
},
file: {
id: 'example-file',
}, },
}, },
{ {
@@ -489,10 +495,16 @@ const TEST_INPUTS: Labrinth.TechReview.Internal.RuleInput[] = [
sha512: '0123456789abcdef', sha512: '0123456789abcdef',
}, },
}, },
scope: { project: {
project_id: 'telemetry-project', id: 'telemetry-project',
version_id: 'telemetry-version', types: ['mod'],
file_id: 'telemetry-file', },
version: {
id: 'telemetry-version',
loaders: ['neoforge'],
},
file: {
id: 'telemetry-file',
}, },
}, },
] ]
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE AND user_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "0f3d943e4fc48a94363b77c8a7d36eb1dd626e77331d8278c406df952691be4c"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES\n (1000, 'userland', 'userland@modrinth.com', 'developer'),\n (1001, 'useful', 'useful@modrinth.com', 'developer'),\n (1002, 'Useless', 'useless@modrinth.com', 'developer')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "0ffebf149d50149d0c128478e6267a2296efc16763cdbd5d8c295a1a7ca08021"
}
@@ -0,0 +1,32 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id AS \"id!: DelphiRuleId\", name, rule\n FROM delphi_rules\n WHERE NOT delete_on_next_revision\n ORDER BY priority DESC, id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: DelphiRuleId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "rule",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false
]
},
"hash": "182be4110f45b6be6a9174200a07a887cebbaeab0a8c1da8b008e0c5e94fbc4e"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n status AS \"status: PayoutStatus\"\n FROM payouts\n ORDER BY id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "status: PayoutStatus",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "1adbd24d815107e13bc1440c7a8f4eeff66ab4165a9f4980032e114db4dc1286"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM payouts_values_notifications WHERE notified = FALSE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "20cff8fdf7971e91c9d473b9a4663ce02ca16781e32232ae0fa7a0af1973d3a4"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE mods SET status = 'processing' WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "329c009635ad2a4c214da2f4651b8c14fe329deac983bc2f43ba74e066f9cf8d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES (2100, 'prefix_under_score', 'prefix_under_score@modrinth.com', 'developer')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "38ace0abad724d1111b79a72c7c8518f1272690ce7766dbdc41ec1e98f7f1fec"
}
@@ -1,26 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id AS \"id!: DelphiRuleId\", rule\n FROM delphi_rules\n WHERE NOT delete_on_next_revision\n ORDER BY priority DESC, id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: DelphiRuleId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "rule",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false
]
},
"hash": "3f1aaeed6e394294fa32da7e7f83765fb64bd9499a796470d544466886e1a808"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO users (id, username, email, role)\n VALUES ($1, $2, $3, 'developer')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "5cb65218a2a4a7343130d838f8a78df08d25844f1019d539712b0cb686e4ae81"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO payouts_values_notifications (date_available, user_id, notified)\n VALUES ($1, $2, FALSE)\n ON CONFLICT (date_available, user_id) DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Int8"
]
},
"nullable": []
},
"hash": "6678cd4c51771cfaae2be8021ba66908ea41a06ba858dc5b523aef6aae27b850"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "CREATE SCHEMA public;",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "66868d0b35229abffa3f668eaef19dc0cdef502935dd5939a2f3ab89b0766ad2"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO payouts_values (user_id, mod_id, amount, created, date_available)\n VALUES ($1, NULL, $2, NOW(), $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Numeric",
"Timestamptz"
]
},
"nullable": []
},
"hash": "69a1cb4b7f1115a990d1fc4805d58541fc78e910111c09ba3d50a12d9ca4a9f8"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = ANY($1::bigint[])\n ORDER BY detail.id\n ", "query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = ANY($1::bigint[])\n ORDER BY detail.id\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -79,6 +79,16 @@
}, },
{ {
"ordinal": 12, "ordinal": 12,
"name": "project_types!: Vec<String>",
"type_info": "TextArray"
},
{
"ordinal": 13,
"name": "loaders!: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 14,
"name": "hashes!: Json<BTreeMap<String, String>>", "name": "hashes!: Json<BTreeMap<String, String>>",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@@ -101,8 +111,10 @@
true, true,
true, true,
true, true,
null,
null,
null null
] ]
}, },
"hash": "868d19328921b9a3bd56fa9354e5e75d09c76a3eb3ed9c89b05b9aab13505bf9" "hash": "7e7e2e9f67158b94eb62834cbfd65fd5165d02ebcc0f8454554cbd870e4ddb42"
} }
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN (\n SELECT\n file_id,\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n GROUP BY file_id\n ) file_hashes ON file_hashes.file_id = file.id\n ORDER BY detail.id\n ", "query": "\n SELECT\n detail.id,\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN (\n SELECT\n loader_version.version_id,\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n GROUP BY loader_version.version_id\n ) version_metadata\n ON version_metadata.version_id = version.id\n LEFT JOIN (\n SELECT\n file_id,\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n GROUP BY file_id\n ) file_hashes ON file_hashes.file_id = file.id\n ORDER BY detail.id\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -79,6 +79,16 @@
}, },
{ {
"ordinal": 12, "ordinal": 12,
"name": "project_types!: Vec<String>",
"type_info": "TextArray"
},
{
"ordinal": 13,
"name": "loaders!: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 14,
"name": "hashes!: Json<BTreeMap<String, String>>", "name": "hashes!: Json<BTreeMap<String, String>>",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@@ -99,8 +109,10 @@
false, false,
false, false,
false, false,
null,
null,
null null
] ]
}, },
"hash": "2fb03e1de120cf921146b6dbd35691117412407c4225f966c013fdfef21a82fb" "hash": "8537362d9c11e437ada9ce97bb604666378d1d24981ab8113badded16fb63413"
} }
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE moderation_locks SET locked_at = NOW() - ($1::bigint * INTERVAL '1 minute') - INTERVAL '1 second' WHERE project_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "8ca7f46618bd72cfd5fe1f246d41a31da1885cc22bb61be6853950ba52be9aa7"
}
@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "\n SELECT\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = $1\n ", "query": "\n SELECT\n detail.key,\n issue.issue_type,\n detail.severity AS \"severity: DelphiSeverity\",\n detail.jar,\n detail.file_path,\n detail.data AS \"data: Json<HashMap<String, serde_json::Value>>\",\n report.delphi_version,\n file.size AS \"size?\",\n file.id AS \"file_id?\",\n version.id AS \"version_id?\",\n version.mod_id AS \"project_id?\",\n COALESCE(version_metadata.project_types, ARRAY[]::text[])\n AS \"project_types!: Vec<String>\",\n COALESCE(version_metadata.loaders, ARRAY[]::text[])\n AS \"loaders!: Vec<String>\",\n COALESCE(file_hashes.hashes, '{}'::jsonb)\n AS \"hashes!: Json<BTreeMap<String, String>>\"\n FROM delphi_report_issue_details detail\n INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n INNER JOIN delphi_reports report ON report.id = issue.report_id\n LEFT JOIN files file ON file.id = report.file_id\n LEFT JOIN versions version ON version.id = file.version_id\n LEFT JOIN LATERAL (\n SELECT\n ARRAY_AGG(\n DISTINCT project_type.name::text\n ORDER BY project_type.name::text\n ) FILTER (WHERE project_type.name IS NOT NULL)\n AS project_types,\n ARRAY_AGG(\n DISTINCT loader.loader\n ORDER BY loader.loader\n ) AS loaders\n FROM loaders_versions loader_version\n INNER JOIN loaders loader\n ON loader.id = loader_version.loader_id\n LEFT JOIN loaders_project_types loader_project_type\n ON loader_project_type.joining_loader_id = loader_version.loader_id\n LEFT JOIN project_types project_type\n ON project_type.id = loader_project_type.joining_project_type_id\n WHERE loader_version.version_id = version.id\n ) version_metadata ON TRUE\n LEFT JOIN LATERAL (\n SELECT\n jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes\n FROM hashes\n WHERE hashes.file_id = file.id\n ) file_hashes ON TRUE\n WHERE detail.id = $1\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -74,6 +74,16 @@
}, },
{ {
"ordinal": 11, "ordinal": 11,
"name": "project_types!: Vec<String>",
"type_info": "TextArray"
},
{
"ordinal": 12,
"name": "loaders!: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 13,
"name": "hashes!: Json<BTreeMap<String, String>>", "name": "hashes!: Json<BTreeMap<String, String>>",
"type_info": "Jsonb" "type_info": "Jsonb"
} }
@@ -95,8 +105,10 @@
false, false,
false, false,
false, false,
null,
null,
null null
] ]
}, },
"hash": "1f318b0dda5aaf58b9e403f87fcacfcd223bd67f6d5ba95868ace4373a98c67d" "hash": "b536d7870ba77ace55e309938c3ba93c7016b2a6f18aa4f22ea57f604d82c1da"
} }
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT status AS \"status: PayoutStatus\" FROM payouts WHERE id = 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "status: PayoutStatus",
"type_info": "Varchar"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "b92b5bb7d179c4fcdbc45600ccfd2402f52fea71e27b08e7926fcc2a9e62c0f3"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DROP SCHEMA public CASCADE;",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c65295f61b18f7be4fdf30b3e2c2c36b9db3770508be4acbb6b441ed8006d9c7"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE users SET email_verified = TRUE WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "cb626a36deffd73e67de2dc4789bd875675779a173fb2cd41ba365c1acca96f9"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, $3, $4, $5, 10.0, NOW())\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Text",
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "cd5ccd618fb3cc41646a6de86f9afedb074492b4ec7f2457c14113f5fd13aa02"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO payouts (id, method, platform_id, status, user_id, amount, created)\n VALUES ($1, $2, NULL, $3, $4, 10.00, NOW())\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "cec4240c7c848988b3dfd13e3f8e5c93783c7641b019fdb698a1ec0be1393606"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT pg_advisory_unlock(1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pg_advisory_unlock",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "fabba52b34e14196b6305671c060d934b9afc6dccc5fc77454fbe45365bdf429"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND body->>'type' = 'payout_available'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "fd5c773a61d35bcd71503ec4d5f86e8917cfab9679d5064074681663ba467e41"
}
@@ -158,8 +158,9 @@ pub async fn test_rule(
})?; })?;
let rule = request.rule; let rule = request.rule;
let expression = rule.clone();
let program = let program =
tokio::task::spawn_blocking(move || cel::Program::compile(&rule)) tokio::task::spawn_blocking(move || cel::Program::compile(&expression))
.await .await
.wrap_internal_err("failed to join cel compilation task")? .wrap_internal_err("failed to join cel compilation task")?
.map_err(|error| { .map_err(|error| {
@@ -168,12 +169,11 @@ pub async fn test_rule(
let mut effects = Vec::with_capacity(request.inputs.len()); let mut effects = Vec::with_capacity(request.inputs.len());
for (index, input) in request.inputs.iter().enumerate() { for (index, input) in request.inputs.iter().enumerate() {
let effect = super::rules_scan::evaluate_rule(&program, input) let evaluation =
.map_err(|error| { super::rules_scan::evaluate_rule(&program, &rule, input);
ApiError::Request(eyre!( let effect = evaluation.wrap_request_err(format!(
"failed to evaluate test input {index}: {error}" "failed to evaluate test input {index}"
)) ))?;
})?;
effects.push(effect); effects.push(effect);
} }
@@ -58,7 +58,9 @@ pub struct RuleInput {
pub trace: RuleTrace, pub trace: RuleTrace,
pub scan: RuleScan, pub scan: RuleScan,
pub artifact: RuleArtifact, pub artifact: RuleArtifact,
pub scope: RuleScope, pub project: RuleProject,
pub version: RuleVersion,
pub file: RuleFile,
} }
#[derive(Deserialize, Serialize, utoipa::ToSchema)] #[derive(Deserialize, Serialize, utoipa::ToSchema)]
@@ -83,14 +85,26 @@ pub struct RuleArtifact {
} }
#[derive(Deserialize, Serialize, utoipa::ToSchema)] #[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct RuleScope { pub struct RuleProject {
pub project_id: Option<String>, pub id: Option<String>,
pub version_id: Option<String>, pub types: Vec<String>,
pub file_id: Option<String>, }
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct RuleVersion {
pub id: Option<String>,
pub loaders: Vec<String>,
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct RuleFile {
pub id: Option<String>,
} }
struct CompiledRule { struct CompiledRule {
id: DelphiRuleId, id: DelphiRuleId,
name: String,
expression: String,
program: cel::Program, program: cel::Program,
} }
@@ -197,6 +211,10 @@ pub async fn get_detail_rule_input(
file.id AS "file_id?", file.id AS "file_id?",
version.id AS "version_id?", version.id AS "version_id?",
version.mod_id AS "project_id?", version.mod_id AS "project_id?",
COALESCE(version_metadata.project_types, ARRAY[]::text[])
AS "project_types!: Vec<String>",
COALESCE(version_metadata.loaders, ARRAY[]::text[])
AS "loaders!: Vec<String>",
COALESCE(file_hashes.hashes, '{}'::jsonb) COALESCE(file_hashes.hashes, '{}'::jsonb)
AS "hashes!: Json<BTreeMap<String, String>>" AS "hashes!: Json<BTreeMap<String, String>>"
FROM delphi_report_issue_details detail FROM delphi_report_issue_details detail
@@ -204,6 +222,26 @@ pub async fn get_detail_rule_input(
INNER JOIN delphi_reports report ON report.id = issue.report_id INNER JOIN delphi_reports report ON report.id = issue.report_id
LEFT JOIN files file ON file.id = report.file_id LEFT JOIN files file ON file.id = report.file_id
LEFT JOIN versions version ON version.id = file.version_id LEFT JOIN versions version ON version.id = file.version_id
LEFT JOIN LATERAL (
SELECT
ARRAY_AGG(
DISTINCT project_type.name::text
ORDER BY project_type.name::text
) FILTER (WHERE project_type.name IS NOT NULL)
AS project_types,
ARRAY_AGG(
DISTINCT loader.loader
ORDER BY loader.loader
) AS loaders
FROM loaders_versions loader_version
INNER JOIN loaders loader
ON loader.id = loader_version.loader_id
LEFT JOIN loaders_project_types loader_project_type
ON loader_project_type.joining_loader_id = loader_version.loader_id
LEFT JOIN project_types project_type
ON project_type.id = loader_project_type.joining_project_type_id
WHERE loader_version.version_id = version.id
) version_metadata ON TRUE
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes
@@ -237,10 +275,16 @@ pub async fn get_detail_rule_input(
size: detail.size, size: detail.size,
hashes: detail.hashes.0, hashes: detail.hashes.0,
}, },
scope: RuleScope { project: RuleProject {
project_id: detail.project_id.map(|id| to_base62(id as u64)), id: detail.project_id.map(|id| to_base62(id as u64)),
version_id: detail.version_id.map(|id| to_base62(id as u64)), types: detail.project_types,
file_id: detail.file_id.map(|id| to_base62(id as u64)), },
version: RuleVersion {
id: detail.version_id.map(|id| to_base62(id as u64)),
loaders: detail.loaders,
},
file: RuleFile {
id: detail.file_id.map(|id| to_base62(id as u64)),
}, },
})) }))
} }
@@ -331,7 +375,7 @@ pub async fn scan_rules(
} }
Err(error) => { Err(error) => {
tracing::error!(error = ?error, "delphi rule scan failed"); tracing::error!(error = ?error, "delphi rule scan failed");
let message = error.to_string(); let message = format!("{error:#}");
let event = RuleScanErrorEvent { message: &message }; let event = RuleScanErrorEvent { message: &message };
if let Ok(data) = serde_json::to_string(&event) { if let Ok(data) = serde_json::to_string(&event) {
let _ = sender.send(Bytes::from(format!( let _ = sender.send(Bytes::from(format!(
@@ -402,6 +446,10 @@ async fn run_scan(
file.id AS "file_id?", file.id AS "file_id?",
version.id AS "version_id?", version.id AS "version_id?",
version.mod_id AS "project_id?", version.mod_id AS "project_id?",
COALESCE(version_metadata.project_types, ARRAY[]::text[])
AS "project_types!: Vec<String>",
COALESCE(version_metadata.loaders, ARRAY[]::text[])
AS "loaders!: Vec<String>",
COALESCE(file_hashes.hashes, '{}'::jsonb) COALESCE(file_hashes.hashes, '{}'::jsonb)
AS "hashes!: Json<BTreeMap<String, String>>" AS "hashes!: Json<BTreeMap<String, String>>"
FROM delphi_report_issue_details detail FROM delphi_report_issue_details detail
@@ -409,6 +457,28 @@ async fn run_scan(
INNER JOIN delphi_reports report ON report.id = issue.report_id INNER JOIN delphi_reports report ON report.id = issue.report_id
LEFT JOIN files file ON file.id = report.file_id LEFT JOIN files file ON file.id = report.file_id
LEFT JOIN versions version ON version.id = file.version_id LEFT JOIN versions version ON version.id = file.version_id
LEFT JOIN (
SELECT
loader_version.version_id,
ARRAY_AGG(
DISTINCT project_type.name::text
ORDER BY project_type.name::text
) FILTER (WHERE project_type.name IS NOT NULL)
AS project_types,
ARRAY_AGG(
DISTINCT loader.loader
ORDER BY loader.loader
) AS loaders
FROM loaders_versions loader_version
INNER JOIN loaders loader
ON loader.id = loader_version.loader_id
LEFT JOIN loaders_project_types loader_project_type
ON loader_project_type.joining_loader_id = loader_version.loader_id
LEFT JOIN project_types project_type
ON project_type.id = loader_project_type.joining_project_type_id
GROUP BY loader_version.version_id
) version_metadata
ON version_metadata.version_id = version.id
LEFT JOIN ( LEFT JOIN (
SELECT SELECT
file_id, file_id,
@@ -458,19 +528,29 @@ async fn run_scan(
size: detail.size, size: detail.size,
hashes: detail.hashes.0, hashes: detail.hashes.0,
}, },
scope: RuleScope { project: RuleProject {
project_id: detail.project_id.map(|id| to_base62(id as u64)), id: detail.project_id.map(|id| to_base62(id as u64)),
version_id: detail.version_id.map(|id| to_base62(id as u64)), types: detail.project_types,
file_id: detail.file_id.map(|id| to_base62(id as u64)), },
version: RuleVersion {
id: detail.version_id.map(|id| to_base62(id as u64)),
loaders: detail.loaders,
},
file: RuleFile {
id: detail.file_id.map(|id| to_base62(id as u64)),
}, },
}; };
for rule in &rules { for rule in &rules {
let effect = evaluate_rule(&rule.program, &input) let effect = evaluate_rule(
&rule.program,
&rule.expression,
&input,
)
.wrap_err_with(|| { .wrap_err_with(|| {
format!( format!(
"failed to evaluate delphi rule {} for detail {detail_id}", "failed to evaluate delphi rule '{}' for detail {detail_id}",
rule.id.0 rule.name
) )
})?; })?;
if let Some(effect) = effect { if let Some(effect) = effect {
@@ -642,6 +722,10 @@ pub(crate) async fn materialize_current_rule_effects(
file.id AS "file_id?", file.id AS "file_id?",
version.id AS "version_id?", version.id AS "version_id?",
version.mod_id AS "project_id?", version.mod_id AS "project_id?",
COALESCE(version_metadata.project_types, ARRAY[]::text[])
AS "project_types!: Vec<String>",
COALESCE(version_metadata.loaders, ARRAY[]::text[])
AS "loaders!: Vec<String>",
COALESCE(file_hashes.hashes, '{}'::jsonb) COALESCE(file_hashes.hashes, '{}'::jsonb)
AS "hashes!: Json<BTreeMap<String, String>>" AS "hashes!: Json<BTreeMap<String, String>>"
FROM delphi_report_issue_details detail FROM delphi_report_issue_details detail
@@ -649,6 +733,26 @@ pub(crate) async fn materialize_current_rule_effects(
INNER JOIN delphi_reports report ON report.id = issue.report_id INNER JOIN delphi_reports report ON report.id = issue.report_id
LEFT JOIN files file ON file.id = report.file_id LEFT JOIN files file ON file.id = report.file_id
LEFT JOIN versions version ON version.id = file.version_id LEFT JOIN versions version ON version.id = file.version_id
LEFT JOIN LATERAL (
SELECT
ARRAY_AGG(
DISTINCT project_type.name::text
ORDER BY project_type.name::text
) FILTER (WHERE project_type.name IS NOT NULL)
AS project_types,
ARRAY_AGG(
DISTINCT loader.loader
ORDER BY loader.loader
) AS loaders
FROM loaders_versions loader_version
INNER JOIN loaders loader
ON loader.id = loader_version.loader_id
LEFT JOIN loaders_project_types loader_project_type
ON loader_project_type.joining_loader_id = loader_version.loader_id
LEFT JOIN project_types project_type
ON project_type.id = loader_project_type.joining_project_type_id
WHERE loader_version.version_id = version.id
) version_metadata ON TRUE
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes jsonb_object_agg(algorithm, encode(hash, 'hex')) AS hashes
@@ -683,19 +787,25 @@ pub(crate) async fn materialize_current_rule_effects(
size: detail.size, size: detail.size,
hashes: detail.hashes.0, hashes: detail.hashes.0,
}, },
scope: RuleScope { project: RuleProject {
project_id: detail.project_id.map(|id| to_base62(id as u64)), id: detail.project_id.map(|id| to_base62(id as u64)),
version_id: detail.version_id.map(|id| to_base62(id as u64)), types: detail.project_types,
file_id: detail.file_id.map(|id| to_base62(id as u64)), },
version: RuleVersion {
id: detail.version_id.map(|id| to_base62(id as u64)),
loaders: detail.loaders,
},
file: RuleFile {
id: detail.file_id.map(|id| to_base62(id as u64)),
}, },
}; };
for rule in &rules { for rule in &rules {
let effect = let effect = evaluate_rule(&rule.program, &rule.expression, &input)
evaluate_rule(&rule.program, &input).wrap_err_with(|| { .wrap_err_with(|| {
format!( format!(
"failed to evaluate delphi rule {} for detail {}", "failed to evaluate delphi rule '{}' for detail {}",
rule.id.0, detail.id rule.name, detail.id
) )
})?; })?;
if let Some(effect) = effect { if let Some(effect) = effect {
@@ -717,7 +827,7 @@ async fn fetch_compiled_rules(
) -> Result<Vec<CompiledRule>> { ) -> Result<Vec<CompiledRule>> {
let rules = sqlx::query!( let rules = sqlx::query!(
r#" r#"
SELECT id AS "id!: DelphiRuleId", rule SELECT id AS "id!: DelphiRuleId", name, rule
FROM delphi_rules FROM delphi_rules
WHERE NOT delete_on_next_revision WHERE NOT delete_on_next_revision
ORDER BY priority DESC, id ORDER BY priority DESC, id
@@ -734,12 +844,14 @@ async fn fetch_compiled_rules(
let program = let program =
cel::Program::compile(&rule.rule).map_err(|error| { cel::Program::compile(&rule.rule).map_err(|error| {
eyre!( eyre!(
"failed to compile delphi rule {}: {error}", "failed to compile delphi rule '{}': {error}",
rule.id.0 rule.name
) )
})?; })?;
Ok(CompiledRule { Ok(CompiledRule {
id: rule.id, id: rule.id,
name: rule.name,
expression: rule.rule,
program, program,
}) })
}) })
@@ -799,6 +911,21 @@ async fn insert_materialized_effects(
} }
pub(super) fn evaluate_rule( pub(super) fn evaluate_rule(
program: &cel::Program,
expression: &str,
input: &RuleInput,
) -> Result<Option<DelphiRuleEffect>> {
evaluate_rule_inner(program, input)
.wrap_err_with(|| {
let input = serde_json::to_string(input).unwrap_or_else(|error| {
format!("<failed to serialize CEL input: {error}>")
});
format!("CEL input: {input}")
})
.wrap_err_with(|| format!("CEL expression: {expression}"))
}
fn evaluate_rule_inner(
program: &cel::Program, program: &cel::Program,
input: &RuleInput, input: &RuleInput,
) -> Result<Option<DelphiRuleEffect>> { ) -> Result<Option<DelphiRuleEffect>> {
@@ -816,8 +943,14 @@ pub(super) fn evaluate_rule(
.add_variable("artifact", &input.artifact) .add_variable("artifact", &input.artifact)
.wrap_err("failed to add `artifact` to cel context")?; .wrap_err("failed to add `artifact` to cel context")?;
context context
.add_variable("scope", &input.scope) .add_variable("project", &input.project)
.wrap_err("failed to add `scope` to cel context")?; .wrap_err("failed to add `project` to cel context")?;
context
.add_variable("version", &input.version)
.wrap_err("failed to add `version` to cel context")?;
context
.add_variable("file", &input.file)
.wrap_err("failed to add `file` to cel context")?;
let value = program let value = program
.execute(&context) .execute(&context)
@@ -2335,10 +2335,16 @@ export namespace Labrinth {
size: number | null size: number | null
hashes: Record<string, string> hashes: Record<string, string>
} }
scope: { project: {
project_id: string | null id: string | null
version_id: string | null types: string[]
file_id: string | null }
version: {
id: string | null
loaders: string[]
}
file: {
id: string | null
} }
} }
-10
View File
@@ -1,10 +0,0 @@
[package]
name = "delphi-cel-experiment"
version = "0.1.0"
edition.workspace = true
publish = false
[dependencies]
cel = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
-174
View File
@@ -1,174 +0,0 @@
use std::{collections::BTreeMap, error::Error, io};
use cel::{Context, Program};
use serde::{Deserialize, Serialize};
const EFFECT_RULE: &str = r#"
trace.issue_type == "OBFUSCATED_NAMES"
&& trace.severity == "high"
&& "confidence" in trace.data
&& trace.data.confidence >= 0.9
? {
"severity": "low"
}
: null
"#;
#[derive(Debug, Serialize)]
struct RuleInput {
schema_version: u32,
trace: TraceInput,
scan: ScanInput,
artifact: ArtifactInput,
scope: ScopeInput,
}
#[derive(Debug, Serialize)]
struct TraceInput {
key: String,
issue_type: String,
severity: DelphiSeverity,
jar: Option<String>,
file_path: String,
data: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize)]
struct ScanInput {
delphi_version: i32,
}
#[derive(Debug, Serialize)]
struct ArtifactInput {
size: u32,
hashes: BTreeMap<String, String>,
}
#[derive(Debug, Serialize)]
struct ScopeInput {
project_id: String,
version_id: String,
file_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
enum DelphiSeverity {
Hidden,
Low,
Medium,
High,
Severe,
Malware,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
struct RuleEffect {
severity: DelphiSeverity,
}
fn main() -> Result<(), Box<dyn Error>> {
let input = example_input();
println!(
"CEL input context:\n{}",
serde_json::to_string_pretty(&input)?
);
let effect = evaluate_rule(EFFECT_RULE, &input)?;
println!("\nCEL map decoded as RuleEffect:\n{effect:#?}");
Ok(())
}
fn example_input() -> RuleInput {
RuleInput {
schema_version: 1,
trace: TraceInput {
key: "known-safe:obfuscated-bootstrap".to_string(),
issue_type: "OBFUSCATED_NAMES".to_string(),
severity: DelphiSeverity::High,
jar: Some("META-INF/jars/embedded.jar".to_string()),
file_path: "com/example/Bootstrap.class".to_string(),
data: BTreeMap::from([
("confidence".to_string(), serde_json::json!(0.97)),
("symbol_count".to_string(), serde_json::json!(42)),
]),
},
scan: ScanInput { delphi_version: 17 },
artifact: ArtifactInput {
size: 412_892,
hashes: BTreeMap::from([
("sha1".to_string(), "0123456789abcdef".to_string()),
("sha512".to_string(), "fedcba9876543210".to_string()),
]),
},
scope: ScopeInput {
project_id: "AANobbMI".to_string(),
version_id: "IIJJKKLL".to_string(),
file_id: "XXYYZZ00".to_string(),
},
}
}
fn evaluate_rule(
expression: &str,
input: &RuleInput,
) -> Result<Option<RuleEffect>, Box<dyn Error>> {
let mut context = Context::default();
context.add_variable("schema_version", input.schema_version)?;
context.add_variable("trace", &input.trace)?;
context.add_variable("scan", &input.scan)?;
context.add_variable("artifact", &input.artifact)?;
context.add_variable("scope", &input.scope)?;
let value = Program::compile(expression)?.execute(&context)?;
let json = value
.json()
.map_err(|error| invalid_data(error.to_string()))?;
match json {
serde_json::Value::Null => Ok(None),
value => Ok(Some(serde_json::from_value(value)?)),
}
}
fn invalid_data(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_a_map_as_an_effect() {
let effect = evaluate_rule(EFFECT_RULE, &example_input())
.expect("rule should evaluate");
assert_eq!(
effect,
Some(RuleEffect {
severity: DelphiSeverity::Low,
})
);
}
#[test]
fn returns_none_when_a_rule_does_not_match() {
let mut input = example_input();
input.trace.issue_type = "NETWORK_ACCESS".to_string();
let effect =
evaluate_rule(EFFECT_RULE, &input).expect("rule should evaluate");
assert_eq!(effect, None);
}
#[test]
fn rejects_unknown_map_fields() {
let error = evaluate_rule(r#"{"unknown": true}"#, &example_input())
.expect_err("unknown fields should be rejected");
assert!(error.to_string().contains("unknown field"));
}
}