Compare commits

...
Author SHA1 Message Date
aecsocket e9f245cacd impl url parsing stuff 2026-08-26 14:32:56 +01:00
aecsocket 641ad9942a cel #bind directive 2026-08-26 13:29:21 +01:00
aecsocket 58f89d9940 prepr 2026-08-25 17:06:31 +01:00
aecsocket 30477f3316 simple autocomplete 2026-08-25 17:03:06 +01:00
aecsocket a7196deb98 cel #define preprocessor, regex ext 2026-08-25 13:57:14 +01:00
aecsocket 9f28a9ba5f enable cel regex 2026-08-25 13:28:42 +01:00
aecsocket b5b7e9bc42 allow modifying test trace in rules.vue 2026-08-25 13:16:59 +01:00
aecsocket 0af562ef6f Merge branch 'main' into boris/dev-1205-trace-rules 2026-08-22 18:23:19 +09:00
aecsocket e9200c7483 update frontend 2026-08-22 18:23:13 +09:00
aecsocket 8c2611805b Merge branch 'main' into boris/dev-1205-trace-rules 2026-08-19 15:56:58 +09:00
aecsocket c204e00ebc add sibling traces 2026-08-19 15:56:47 +09:00
aecsocket 5cf9d423ad fix up wrap err calls 2026-08-13 18:27:30 +00:00
aecsocket ac00e2bd5d allow returning strings from CEL as shorthand 2026-08-13 18:12:46 +00:00
aecsocket 43818e4f90 add dummy fixture 2026-08-13 18:03:56 +00:00
aecsocket 7ef2492d03 prepare 2026-08-13 17:50:07 +00:00
aecsocket 37d371e46c Merge branch 'main' into boris/dev-1205-trace-rules 2026-08-13 17:14:18 +00:00
aecsocket 26e1d645b8 live/outdated label on rules 2026-08-12 09:29:23 +00:00
aecsocket 04d00c0ba0 improve errors for CEL input 2026-08-12 08:12:44 +00:00
aecsocket 7f59feeb98 Merge branch 'main' of github.com:modrinth/code into boris/dev-1205-trace-rules
# Conflicts:
#	apps/frontend/src/components/ui/moderation/GlobalDetailLocalTraceCard.vue
#	apps/frontend/src/pages/moderation.vue
#	apps/labrinth/.sqlx/query-06384971752b20c04d7a747777bf03a765ab60c2b029a47fcd3fe97c02ee5b2a.json
#	apps/labrinth/.sqlx/query-1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005.json
#	apps/labrinth/.sqlx/query-33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5.json
#	apps/labrinth/.sqlx/query-4680c4a59c6679f90e3b9e1a33ed1cb1fb60b93ffb79ba5b99e01ee0c14c991a.json
#	apps/labrinth/.sqlx/query-5a68c53bd00c08edf9dfb9ffdabc28f86cdd77cd06bbb10c9a8e5199509f9c4b.json
#	apps/labrinth/.sqlx/query-87d30e8802ebe69858141d0d918cafb5286f984cfe55ad1a7ac2219ec50539a4.json
#	apps/labrinth/.sqlx/query-aedd5d9fe052a9fa70e585f389c3b4915a88cb9c3ec54493a2a29582c80b5c76.json
#	apps/labrinth/.sqlx/query-befd8c75e7ed4621f5e99d88719c0fde9d617b6603b99b75cc18d1a02387bac4.json
#	apps/labrinth/.sqlx/query-c268a2a256f11a65449bd132a14f931c2e9776ffed87f4406bb5c2bc30087f5b.json
#	apps/labrinth/.sqlx/query-c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73.json
#	apps/labrinth/.sqlx/query-d37041da5452ecb82da506a83154cd66b78ebb9a1dbf0bb46e5bd6bb2b3a9bb9.json
#	apps/labrinth/.sqlx/query-d3d320af2786bf7908e65f5b7a35ade08110fb341d694ca0f25eecca2f5ad5bf.json
#	apps/labrinth/src/routes/internal/delphi/tech_review_sync.rs
#	apps/labrinth/src/routes/internal/moderation/tech_review/global.rs
2026-08-11 06:35:49 +00:00
aecsocket 4c8abfb78d adjust schema 2026-08-02 17:05:41 +01:00
aecsocket 5ead1499cd flatten input context 2026-08-02 15:13:13 +01:00
aecsocket ddb45f6aac move hidden to severity 2026-08-02 15:08:27 +01:00
aecsocket cc77fd1b87 Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-30 05:15:25 +01:00
aecsocket b92fc10e54 cleanup 2026-07-27 17:47:17 +01:00
aecsocket 849943ed4e clean up db stuff 2026-07-27 10:41:26 +01:00
aecsocket 18c95e2f61 prepr 2026-07-27 10:19:54 +01:00
aecsocket 7c7efd5efe Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-27 10:05:50 +01:00
aecsocket 4c5e1074fd fix 2026-07-26 15:36:41 +01:00
aecsocket 36d3aab0de fmt 2026-07-26 15:26:02 +01:00
aecsocket 88c0915066 Merge branch 'main' into boris/dev-1205-trace-rules 2026-07-26 15:20:26 +01:00
aecsocket f0dff8ebd5 clean up __dummy stuff 2026-07-26 15:17:50 +01:00
aecsocket 8d82c228cf rules affect tech review detail statuses 2026-07-24 13:49:20 +01:00
aecsocket 4b07ed720e remove old ui 2026-07-23 09:12:37 +01:00
aecsocket ea42c644f3 show in/out schema 2026-07-22 20:14:57 +01:00
aecsocket 28154689f4 run rule scans 2026-07-22 19:34:18 +01:00
aecsocket 3c77cc1502 use Ace editor 2026-07-22 18:21:33 +01:00
aecsocket b6b19e543c add demo delphi rules ui 2026-07-22 17:43:03 +01:00
aecsocket 023c789a31 wip: delphi trace transform rules 2026-07-19 09:46:12 +01:00
97 changed files with 6488 additions and 1009 deletions
Generated
+80 -3
View File
@@ -437,6 +437,23 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "antlr4rust"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093d520274bfff7278d776f7ea12981a0a0a6f96db90964658e0f38fc6e9a6a6"
dependencies = [
"better_any",
"bit-set 0.8.0",
"byteorder",
"lazy_static",
"murmur3",
"once_cell",
"parking_lot",
"typed-arena",
"uuid 1.23.3",
]
[[package]]
name = "anyhow"
version = "1.0.100"
@@ -1421,10 +1438,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c"
dependencies = [
"base64 0.21.7",
"pastey",
"pastey 0.1.1",
"serde",
]
[[package]]
name = "better_any"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4372b9543397a4b86050cc5e7ee36953edf4bac9518e8a774c2da694977fb6e4"
[[package]]
name = "bindgen"
version = "0.72.1"
@@ -1451,7 +1474,16 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec 0.8.0",
]
[[package]]
@@ -1460,6 +1492,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bit_field"
version = "0.10.3"
@@ -1889,6 +1927,23 @@ dependencies = [
"shlex",
]
[[package]]
name = "cel"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed39583e427bf41d93c28c7f27c943a93bcb8697220ff3575fd53a9e13f3814"
dependencies = [
"antlr4rust",
"base64 0.22.1",
"lazy_static",
"nom 7.1.3",
"pastey 0.2.3",
"regex",
"serde",
"serde_json",
"thiserror 1.0.69",
]
[[package]]
name = "censor"
version = "0.3.0"
@@ -3530,7 +3585,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
]
@@ -5483,6 +5538,7 @@ dependencies = [
"base64 0.22.1",
"bitflags 2.9.4",
"bytes",
"cel",
"censor",
"chrono",
"chumsky",
@@ -6170,6 +6226,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb585ade2549a017db2e35978b77c319214fa4b37cede841e27954dd6e8f3ca8"
[[package]]
name = "murmur3"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a198f9589efc03f544388dfc4a19fe8af4323662b62f598b8dcfdac62c14771c"
dependencies = [
"byteorder",
]
[[package]]
name = "mutually_exclusive_features"
version = "0.1.0"
@@ -7192,6 +7257,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pastey"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]]
name = "path-util"
version = "0.0.0"
@@ -11863,6 +11934,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typed-path"
version = "0.12.0"
+1
View File
@@ -57,6 +57,7 @@ bitflags = "2.9.4"
bon = "3.9.3"
bytemuck = "1.24.0"
bytes = "1.10.1"
cel = { version = "0.14.0", default-features = false, features = ["json", "regex"] }
censor = "0.3.0"
chardetng = "0.1.17"
chrono = "0.4.42"
@@ -5,14 +5,8 @@
<p class="m-0 break-words font-semibold text-contrast">
{{ trace.project_name }}
</p>
<p class="m-0 mt-1 flex flex-wrap items-center gap-1 text-sm text-secondary">
<span class="break-all">{{ trace.version_number }}</span>
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
<span class="break-all">{{ decodeTracePath(trace.file_name) }}</span>
<template v-if="trace.jar">
<ChevronRightIcon class="size-4 shrink-0" aria-hidden="true" />
<span class="break-all">{{ decodeTracePath(trace.jar) }}</span>
</template>
<p class="m-0 mt-1 text-sm text-secondary">
<IssueDetailPath :segments="[trace.version_number, trace.file_name, trace.jar]" />
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
@@ -31,9 +25,11 @@
<script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client'
import { ChevronRightIcon, ExternalIcon } from '@modrinth/assets'
import { ExternalIcon } from '@modrinth/assets'
import { Badge, ButtonLink } from '@modrinth/ui'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const props = defineProps<{
trace: Labrinth.TechReview.Internal.GlobalIssueDetailTrace
}>()
@@ -44,12 +40,4 @@ const localTraceLink = computed(
props.trace.detail_id,
)}`,
)
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
</script>
@@ -63,7 +63,7 @@
</p>
<p class="m-0 break-all text-secondary">
<span class="font-semibold text-contrast">Path</span>
{{ decodeTracePath(getLatestLocalTrace(trace)?.file_path ?? '') }}
<IssueDetailPath :segments="[getLatestLocalTrace(trace)?.file_path]" />
</p>
</div>
</div>
@@ -131,6 +131,7 @@ import {
} from '@modrinth/ui'
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
@@ -163,24 +164,19 @@ function getLatestLocalTrace(trace: Labrinth.TechReview.Internal.GlobalIssueDeta
return trace.local_traces.at(-1)
}
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function getSeverityBadgeColor(
severity: Labrinth.TechReview.Internal.DelphiSeverity | undefined,
): string {
switch (severity) {
case 'malware':
case 'severe':
return 'border-red/60 bg-highlight-red text-red'
case 'high':
return 'border-orange/60 bg-highlight-orange text-orange'
case 'medium':
return 'border-green/60 bg-highlight-green text-green'
case 'hidden':
return 'border-divider bg-surface-2 text-secondary'
case 'low':
default:
return 'border-blue/60 bg-highlight-blue text-blue'
@@ -0,0 +1,74 @@
<template>
<span class="inline-flex min-w-0 flex-wrap items-center gap-1">
<template v-for="(segment, index) in decodedSegments" :key="`${segment}-${index}`">
<ChevronRightIcon v-if="index > 0" class="size-4 shrink-0" aria-hidden="true" />
<span
v-tooltip="isTruncated(segment) ? segment : undefined"
class="break-all"
:class="{
'font-semibold text-contrast': emphasizeLast && index === decodedSegments.length - 1,
'text-secondary': emphasizeLast && index < decodedSegments.length - 1,
}"
>
{{ formatSegment(segment) }}
</span>
</template>
</span>
</template>
<script setup lang="ts">
import { ChevronRightIcon } from '@modrinth/assets'
const props = withDefaults(
defineProps<{
segments: readonly (string | null | undefined)[]
truncate?: boolean
maxLength?: number
emphasizeLast?: boolean
decode?: boolean
hideBaseMrpack?: boolean
}>(),
{
truncate: false,
maxLength: 120,
emphasizeLast: false,
decode: true,
hideBaseMrpack: false,
},
)
const decodedSegments = computed(() => {
const segments = props.segments
.flatMap((segment) => segment?.split('#') ?? [])
.filter((segment) => segment.length > 0)
.map((segment) => (props.decode ? decodePath(segment) : segment))
if (props.hideBaseMrpack && segments[0]?.toLowerCase().endsWith('.mrpack')) {
return segments.slice(1)
}
return segments
})
function decodePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function isTruncated(segment: string): boolean {
return props.truncate && segment.length > props.maxLength
}
function formatSegment(segment: string): string {
if (!isTruncated(segment)) return segment
const separator = '...'
const charsToShow = props.maxLength - separator.length
const frontChars = Math.ceil(charsToShow / 3)
const backChars = Math.floor((charsToShow * 2) / 3)
return segment.slice(0, frontChars) + separator + segment.slice(-backChars)
}
</script>
@@ -6,7 +6,6 @@ import {
CheckCheckIcon,
CheckIcon,
ChevronDownIcon,
ChevronRightIcon,
ClipboardCopyIcon,
CodeIcon,
CopyIcon,
@@ -48,6 +47,7 @@ import {
import dayjs from 'dayjs'
import { computed, nextTick, reactive, ref, watch } from 'vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
import type { UnsafeFile } from '~/components/ui/moderation/MaliciousSummaryModal.vue'
import ThreadView from '~/components/ui/thread/ThreadView.vue'
@@ -227,7 +227,10 @@ async function updateGlobalIssueDetail(
])
}
const severityOrder = { severe: 3, high: 2, medium: 1, low: 0 } as Record<string, number>
const severityOrder = { malware: 4, severe: 3, high: 2, medium: 1, low: 0, hidden: -1 } as Record<
string,
number
>
type DetailDecision = 'safe' | 'malware' | 'pending'
type DetailDecisionScope = 'local' | 'global'
@@ -491,12 +494,15 @@ function handleTabClick(index: number) {
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
switch (severity) {
case 'malware':
case 'severe':
return 'border-red/60 border bg-highlight-red text-red'
case 'high':
return 'border-orange/60 border bg-highlight-orange text-orange'
case 'medium':
return 'border-green/60 border bg-highlight-green text-green'
case 'hidden':
return 'border-divider border bg-surface-2 text-secondary'
case 'low':
default:
return 'border-blue/60 border bg-highlight-blue text-blue'
@@ -515,12 +521,15 @@ function truncateMiddle(str: string, maxLength: number = 120): string {
const severityColor = computed(() => {
switch (highestSeverity.value) {
case 'malware':
case 'severe':
return 'text-red bg-highlight-red border-solid border-[1px] border-red'
case 'high':
return 'text-orange bg-highlight-orange border-solid border-[1px] border-orange'
case 'medium':
return 'text-green bg-highlight-green border-solid border-[1px] border-green'
case 'hidden':
return 'text-secondary bg-surface-2 border-solid border-[1px] border-divider'
case 'low':
default:
return 'text-blue bg-highlight-blue border-solid border-[1px] border-blue'
@@ -604,6 +613,29 @@ async function copyToClipboard(code: string, detailId: string) {
}
}
async function copyDetailCelInput(detailId: string) {
if (copyingCelDetails.has(detailId)) return
copyingCelDetails.add(detailId)
try {
const input = await client.labrinth.tech_review_internal.getDetailRuleInput(detailId)
await navigator.clipboard.writeText(JSON.stringify(input, null, 2))
copiedCelDetails.add(detailId)
setTimeout(() => {
copiedCelDetails.delete(detailId)
}, 2000)
} catch (error) {
console.error('Failed to copy CEL input:', error)
addNotification({
type: 'error',
title: 'Failed to copy CEL input',
text: 'An error occurred while loading the trace rule input.',
})
} finally {
copyingCelDetails.delete(detailId)
}
}
function getDetailDecision(
detailId: string,
backendStatus: Labrinth.TechReview.Internal.DelphiReportIssueStatus,
@@ -1010,6 +1042,8 @@ async function updateGlobalDetailStatus(
const expandedClasses = reactive<Set<string>>(new Set())
const autoExpandedFileIds = reactive<Set<string>>(new Set())
const showCopyFeedback = reactive<Map<string, boolean>>(new Map())
const copyingCelDetails = reactive<Set<string>>(new Set())
const copiedCelDetails = reactive<Set<string>>(new Set())
const highlightedSourceCache = reactive<Map<string, { source: string; lines: string[] }>>(new Map())
const LAZY_LOAD_CLASS_SOURCE_MINIMUM = 2
@@ -1036,7 +1070,7 @@ interface JarGroup {
function splitJarSegments(jar: string | null, currentFileName: string | null): string[] {
if (!jar) return []
const segments = jar
.split(/[/#]/)
.split('#')
.map((s) => decodeURIComponent(s.trim()))
.filter((s) => s.length > 0)
// Skip the first segment if it matches the current file tab (it's already shown in the file list)
@@ -1820,27 +1854,12 @@ function copyId() {
class="border-b border-solid border-surface-1 px-4 py-3"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-1">
<template
v-for="(segment, index) in jarGroup.segments"
:key="`${jarGroup.key}-${index}`"
>
<span
class="font-mono text-sm"
:class="
index === jarGroup.segments.length - 1
? 'font-semibold text-contrast'
: 'text-secondary'
"
>
{{ segment }}
</span>
<ChevronRightIcon
v-if="index < jarGroup.segments.length - 1"
class="size-4 text-secondary"
/>
</template>
</div>
<IssueDetailPath
:segments="jarGroup.segments"
:decode="false"
class="font-mono text-sm"
emphasize-last
/>
<div
v-if="getJarRemainingUnmarkedCount(jarGroup) > 0"
@@ -1914,9 +1933,12 @@ function copyId() {
<ChevronDownIcon class="h-5 w-5 text-contrast" />
</IconButton>
<span v-tooltip="classItem.filePath" class="font-mono font-semibold">{{
truncateMiddle(classItem.filePath)
}}</span>
<IssueDetailPath
:segments="[classItem.jar, classItem.filePath]"
class="font-mono font-semibold"
hide-base-mrpack
truncate
/>
<div
class="rounded-full border-solid px-2.5 py-1"
@@ -1991,6 +2013,20 @@ function copyId() {
</div>
<div class="detail-verdict-action-groups">
<Button
:disabled="copyingCelDetails.has(flag.detail.id)"
@click="copyDetailCelInput(flag.detail.id)"
>
<LoaderCircleIcon
v-if="copyingCelDetails.has(flag.detail.id)"
class="animate-spin"
aria-hidden="true"
/>
<ClipboardCopyIcon v-else aria-hidden="true" />
<span aria-live="polite">
{{ copiedCelDetails.has(flag.detail.id) ? 'Copied!' : 'Copy CEL' }}
</span>
</Button>
<div
class="detail-verdict-buttons"
role="group"
@@ -2957,6 +2957,9 @@
"moderation.moderate": {
"message": "Moderate"
},
"moderation.page.delphi-rules": {
"message": "Delphi rules"
},
"moderation.page.external-projects": {
"message": "External projects"
},
+23 -2
View File
@@ -18,7 +18,14 @@
</template>
<script setup lang="ts">
import { FolderIcon, GlobeIcon, HashIcon, ReportIcon, ShieldCheckIcon } from '@modrinth/assets'
import {
FolderIcon,
GlobeIcon,
HashIcon,
ReportIcon,
SettingsIcon,
ShieldCheckIcon,
} from '@modrinth/assets'
import { getMarginTarget } from '@modrinth/moderation'
import { Chips, defineMessages, NavTabs, useVIntl } from '@modrinth/ui'
@@ -58,6 +65,10 @@ const messages = defineMessages({
id: 'moderation.page.global-detail-traces',
defaultMessage: 'Global traces',
},
delphiRulesTitle: {
id: 'moderation.page.delphi-rules',
defaultMessage: 'Delphi rules',
},
})
const moderationLinks = [
@@ -78,6 +89,11 @@ const moderationLinks = [
href: '/moderation/global-traces',
icon: HashIcon,
},
{
label: formatMessage(messages.delphiRulesTitle),
href: '/moderation/technical-review/rules',
icon: SettingsIcon,
},
]
const mobileNavOptions = [
@@ -86,12 +102,15 @@ const mobileNavOptions = [
formatMessage(messages.reportsTitle),
formatMessage(messages.externalFilesTitle),
formatMessage(messages.globalDetailTracesTitle),
formatMessage(messages.delphiRulesTitle),
]
const selectedChip = computed({
get() {
const path = route.path
if (path.startsWith('/moderation/technical-review')) {
if (path.startsWith('/moderation/technical-review/rules')) {
return formatMessage(messages.delphiRulesTitle)
} else if (path.startsWith('/moderation/technical-review')) {
return formatMessage(messages.technicalReviewTitle)
} else if (path.startsWith('/moderation/reports')) {
return formatMessage(messages.reportsTitle)
@@ -117,6 +136,8 @@ function navigateToPage(selectedOption: string) {
router.push('/moderation/external-projects')
} else if (selectedOption === formatMessage(messages.globalDetailTracesTitle)) {
router.push('/moderation/global-traces')
} else if (selectedOption === formatMessage(messages.delphiRulesTitle)) {
router.push('/moderation/technical-review/rules')
} else {
router.push('/moderation')
}
@@ -41,7 +41,7 @@
</p>
<p class="m-0 break-all text-secondary">
<span class="font-semibold text-contrast">Path</span>
{{ decodeTracePath(latestLocalTrace.file_path) }}
<IssueDetailPath :segments="[latestLocalTrace.file_path]" />
</p>
</div>
</div>
@@ -100,6 +100,7 @@ import {
} from '@modrinth/ui'
import GlobalDetailLocalTraceCard from '~/components/ui/moderation/GlobalDetailLocalTraceCard.vue'
import IssueDetailPath from '~/components/ui/moderation/IssueDetailPath.vue'
const client = injectModrinthClient()
const { addNotification } = injectNotificationManager()
@@ -134,22 +135,17 @@ const pageEnd = computed(() =>
)
const latestLocalTrace = computed(() => trace.value?.local_traces.at(-1))
function decodeTracePath(path: string): string {
try {
return decodeURIComponent(path)
} catch {
return path
}
}
function getSeverityBadgeColor(severity: Labrinth.TechReview.Internal.DelphiSeverity): string {
switch (severity) {
case 'malware':
case 'severe':
return 'border-red/60 bg-highlight-red text-red'
case 'high':
return 'border-orange/60 bg-highlight-orange text-orange'
case 'medium':
return 'border-green/60 bg-highlight-green text-green'
case 'hidden':
return 'border-divider bg-surface-2 text-secondary'
case 'low':
default:
return 'border-blue/60 bg-highlight-blue text-blue'
@@ -5,10 +5,12 @@ import {
ListFilterIcon,
LoaderCircleIcon,
SearchIcon,
SettingsIcon,
SortAscIcon,
SortDescIcon,
} from '@modrinth/assets'
import {
ButtonLink,
Combobox,
type ComboboxOption,
commonMessages,
@@ -652,6 +654,11 @@ onUnmounted(() => {
<div
class="flex flex-col items-stretch justify-end gap-2 sm:flex-row sm:items-center lg:flex-shrink-0"
>
<ButtonLink to="/moderation/technical-review/rules">
<SettingsIcon class="size-5" />
Rules
</ButtonLink>
<Combobox
v-model="currentResponseFilter"
class="!w-full flex-grow sm:!w-[120px] sm:flex-grow-0"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tSELECT\n\t\t\tdelphi_rule.id AS \"id!: DelphiRuleId\",\n\t\t\tdelphi_rule.name,\n\t\t\tdelphi_rule.rule,\n\t\t\tdelphi_rule.priority,\n\t\t\tdelphi_rule.revision,\n\t\t\t(\n\t\t\t\tSELECT revision FROM delphi_rule_revisions LIMIT 1\n\t\t\t) AS \"current_revision!\",\n\t\t\tdelphi_rule.created_at,\n\t\t\tdelphi_rule.updated_at,\n\t\t\tdelphi_rule.created_by,\n\t\t\tdelphi_rule.updated_by,\n\t\t\tCOALESCE(preview.affected_details_count, 0)\n\t\t\t\tAS \"affected_details_count!\",\n\t\t\tpreview.detail_id AS \"detail_id?: DelphiReportIssueDetailsId\",\n\t\t\tpreview.issue_id AS \"issue_id?: DelphiReportIssueId\",\n\t\t\tpreview.project_id AS \"project_id?: DBProjectId\",\n\t\t\tpreview.project_name AS \"project_name?\",\n\t\t\tpreview.project_icon_url AS \"project_icon_url?\",\n\t\t\tpreview.version_id AS \"version_id?: DBVersionId\",\n\t\t\tpreview.version_name AS \"version_name?\",\n\t\t\tpreview.version_number AS \"version_number?\",\n\t\t\tpreview.issue_type AS \"issue_type?\",\n\t\t\tpreview.key AS \"key?\",\n\t\t\tpreview.jar AS \"jar?\",\n\t\t\tpreview.file_path AS \"file_path?\",\n\t\t\tpreview.original_severity AS \"original_severity?: DelphiSeverity\",\n\t\t\tpreview.severity AS \"effect_severity?: DelphiSeverity\"\n\t\tFROM delphi_rules delphi_rule\n\t\tLEFT JOIN LATERAL (\n\t\t\tSELECT\n\t\t\t\teffect.detail_id,\n\t\t\t\tdetail.issue_id,\n\t\t\t\tversion.mod_id AS project_id,\n\t\t\t\tproject.name AS project_name,\n\t\t\t\tproject.icon_url AS project_icon_url,\n\t\t\t\tversion.id AS version_id,\n\t\t\t\tversion.name AS version_name,\n\t\t\t\tversion.version_number,\n\t\t\t\tissue.issue_type,\n\t\t\t\tdetail.key,\n\t\t\t\tdetail.jar,\n\t\t\t\tdetail.file_path,\n\t\t\t\tdetail.severity AS original_severity,\n\t\t\t\teffect.severity,\n\t\t\t\tCOUNT(*) OVER () AS affected_details_count\n\t\t\tFROM delphi_rule_effects effect\n\t\t\tINNER JOIN delphi_rule_revisions published\n\t\t\t\tON published.revision = effect.revision\n\t\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\t\tON detail.id = effect.detail_id\n\t\t\tINNER JOIN delphi_report_issues issue\n\t\t\t\tON issue.id = detail.issue_id\n\t\t\tINNER JOIN delphi_reports report\n\t\t\t\tON report.id = issue.report_id\n\t\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\t\tWHERE effect.rule_id = delphi_rule.id\n\t\t\tORDER BY effect.detail_id DESC\n\t\t\tLIMIT 3\n\t\t) preview ON TRUE\n\t\tWHERE NOT delphi_rule.delete_on_next_revision\n\t\tORDER BY\n\t\t\tdelphi_rule.priority DESC,\n\t\t\tdelphi_rule.id,\n\t\t\tpreview.detail_id DESC\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: DelphiRuleId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "rule",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "priority",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "revision",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "current_revision!",
"type_info": "Int8"
},
{
"ordinal": 6,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "created_by",
"type_info": "Int8"
},
{
"ordinal": 9,
"name": "updated_by",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "affected_details_count!",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "detail_id?: DelphiReportIssueDetailsId",
"type_info": "Int8"
},
{
"ordinal": 12,
"name": "issue_id?: DelphiReportIssueId",
"type_info": "Int8"
},
{
"ordinal": 13,
"name": "project_id?: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 14,
"name": "project_name?",
"type_info": "Varchar"
},
{
"ordinal": 15,
"name": "project_icon_url?",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "version_id?: DBVersionId",
"type_info": "Int8"
},
{
"ordinal": 17,
"name": "version_name?",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "version_number?",
"type_info": "Varchar"
},
{
"ordinal": 19,
"name": "issue_type?",
"type_info": "Text"
},
{
"ordinal": 20,
"name": "key?",
"type_info": "Text"
},
{
"ordinal": 21,
"name": "jar?",
"type_info": "Text"
},
{
"ordinal": 22,
"name": "file_path?",
"type_info": "Text"
},
{
"ordinal": 23,
"name": "original_severity?: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
},
{
"ordinal": 24,
"name": "effect_severity?: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
null,
false,
false,
true,
true,
null,
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "020f0da009002bd07c9711d617ea83f82b979397364ac9f61abb9bc8833a0d51"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH project_membership AS (\n SELECT\n detail.project_id,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ) AS old_needs_review,\n BOOL_OR(\n detail.status IN ('pending', 'unsafe')\n AND COALESCE(\n new_effect.severity,\n detail.original_severity\n ) != 'hidden'\n ) AS new_needs_review\n FROM delphi_issue_details_with_statuses detail\n LEFT JOIN delphi_rule_effects new_effect\n ON new_effect.revision = $1\n AND new_effect.detail_id = detail.id\n GROUP BY detail.project_id\n )\n SELECT\n project_id AS \"project_id!: DBProjectId\",\n new_needs_review AS \"new_needs_review!\"\n FROM project_membership\n WHERE old_needs_review IS DISTINCT FROM new_needs_review\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "new_needs_review!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
true,
null
]
},
"hash": "03e377401b1a9bab44f3ff6451d3c1474a73fbce86ee25a3dab2320d59fc72b9"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = $1\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = $1\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "thread_id: DBThreadId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "last_tech_review_message_type",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8",
"TextArray"
]
},
"nullable": [
null,
null
]
},
"hash": "080e2f0068c0c1c248dcfacf39798aefed70d9a61610fc7f4f1c8f291706fa4a"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT revision FROM delphi_rule_revisions LIMIT 1 FOR UPDATE",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "revision",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "0a541871989f0139a7f6f52e8cbae110b88a9cc05985c6cddc325ab913c9731a"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH inserted AS (\n INSERT INTO delphi_tech_review_queue (project_id)\n SELECT unnest($1::bigint[])\n ON CONFLICT (project_id) DO NOTHING\n RETURNING project_id\n )\n SELECT\n inserted.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = inserted.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM inserted\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "thread_id!: DBThreadId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
false,
null
]
},
"hash": "0b94bdb7bf42fdfd0fb1b015fe582bfe8c43582acd17e726616da863e35742c5"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = ANY($1::bigint[])\n ORDER BY didws.issue_id, didws.id\n ",
"query": "\n SELECT\n didws.id AS \"id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n didws.key AS \"key!: String\",\n didws.jar AS \"jar?: String\",\n didws.file_path AS \"file_path!: String\",\n didws.data AS \"data!: sqlx::types::Json<HashMap<String, serde_json::Value>>\",\n didws.severity AS \"severity!: DelphiSeverity\",\n didws.local_status AS \"local_status?: DelphiStatus\",\n didws.global_status AS \"global_status?: DelphiStatus\",\n didws.status AS \"status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = ANY($1::bigint[])\n AND didws.severity != 'hidden'\n ORDER BY didws.issue_id, didws.id\n ",
"describe": {
"columns": [
{
@@ -41,10 +41,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -117,5 +119,5 @@
true
]
},
"hash": "63a9a89443566baa880ddbe05f741e2a83b50d89f5954ab8d951a090e702de73"
"hash": "15d8434ef325e21959ef8ab0541f78f647125717e2d157e0769e80aeae2cdc90"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE delphi_rules SET revision = $1 WHERE NOT delete_on_next_revision",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "1697315d3414e911c9be30fa16164f884d1941120499ba076c7393c942131848"
}
@@ -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"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))\n AND didws.status = 'pending'\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "thread_id!: DBThreadId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text",
"TextArray",
"Text",
"TextArray",
"TextArray"
]
},
"nullable": [
false,
null
]
},
"hash": "1a6d4ac11af078439cff5c772f4dc2392729f99ba1f8c7892831235f341fb276"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM delphi_rules WHERE delete_on_next_revision",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = $1\n RETURNING project_id AS \"project_id: DBProjectId\"\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id: DBProjectId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false
]
},
"hash": "285c66152a506e79c3c8996b4f44b8f63dcf79a2d02dcfeac9655d89bdb2750e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND dri.issue_type != '__dummy'\n ORDER BY didws.id\n LIMIT $3\n ",
"query": "\n SELECT\n didws.key AS \"detail_key!\",\n didws.id AS \"detail_id!: DelphiReportIssueDetailsId\",\n didws.issue_id AS \"issue_id!: DelphiReportIssueId\",\n dri.issue_type,\n m.id AS \"project_id!: DBProjectId\",\n m.slug AS \"project_slug?\",\n m.name AS \"project_name!\",\n v.id AS \"version_id!: DBVersionId\",\n v.version_number,\n f.id AS \"file_id!: DBFileId\",\n f.filename AS \"file_name!\",\n didws.jar AS \"jar?\",\n didws.file_path AS \"file_path!\",\n didws.severity AS \"severity!: DelphiSeverity\",\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS \"local_status!: DelphiStatus\",\n didws.status AS \"effective_status!: DelphiStatus\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = $1\n AND ($2::bigint IS NULL OR didws.id > $2)\n AND didws.severity != 'hidden'\n ORDER BY didws.id\n LIMIT $3\n ",
"describe": {
"columns": [
{
@@ -76,10 +76,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -144,5 +146,5 @@
true
]
},
"hash": "055f71ec6d193c5f5259cafc989a04f3f791205e6c2df15c5bf9d5afcf71a76a"
"hash": "36ffcc9010d1f70deba53483cc2bcbc274f4b55db499bf83b990634b9a805575"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ",
"query": "\n WITH ranked_local_traces AS (\n SELECT\n didws.key AS detail_key,\n didws.id AS detail_id,\n didws.issue_id,\n dri.issue_type,\n m.id AS project_id,\n m.slug AS project_slug,\n m.name AS project_name,\n v.id AS version_id,\n v.version_number,\n f.id AS file_id,\n f.filename AS file_name,\n didws.jar,\n didws.file_path,\n didws.severity,\n COALESCE(didv.verdict, 'pending'::delphi_report_issue_status)\n AS local_status,\n didws.status AS effective_status,\n ROW_NUMBER() OVER (\n PARTITION BY didws.key\n ORDER BY didws.id\n ) AS row_num\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n INNER JOIN delphi_reports dr ON dr.id = dri.report_id\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n INNER JOIN mods m ON m.id = v.mod_id\n LEFT JOIN delphi_issue_detail_verdicts didv\n ON didv.project_id = didws.project_id\n AND didv.detail_key = didws.key\n WHERE\n didws.key = ANY($1::text[])\n AND didws.severity != 'hidden'\n AND dri.issue_type != '__dummy'\n AND (\n $3::text IS NULL\n OR didws.key ILIKE '%' || $3 || '%'\n OR didws.file_path ILIKE '%' || $3 || '%'\n )\n )\n SELECT\n detail_key AS \"detail_key!\",\n detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n issue_id AS \"issue_id!: DelphiReportIssueId\",\n issue_type,\n project_id AS \"project_id!: DBProjectId\",\n project_slug AS \"project_slug?\",\n project_name AS \"project_name!\",\n version_id AS \"version_id!: DBVersionId\",\n v.version_number,\n file_id AS \"file_id!: DBFileId\",\n file_name AS \"file_name!\",\n jar AS \"jar?\",\n file_path AS \"file_path!\",\n severity AS \"severity!: DelphiSeverity\",\n local_status AS \"local_status!: DelphiStatus\",\n effective_status AS \"effective_status!: DelphiStatus\"\n FROM ranked_local_traces v\n WHERE row_num <= $2\n ORDER BY detail_key, detail_id\n ",
"describe": {
"columns": [
{
@@ -76,10 +76,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -144,5 +146,5 @@
true
]
},
"hash": "1551b022217df05490a01e715b3584d26550d01a1917d1af64fe236d63dd5005"
"hash": "3b0d78d18738798fd6be69c040bcb1a9986509413b2115de367a82eb58bc62f3"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT COUNT(*) AS \"count!\"\n FROM delphi_report_issue_details\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "42579ab8ccb58b9b2553787327115af8dbb2a85db0affc67eac74cfb48fbaa3e"
}
@@ -0,0 +1,120 @@
{
"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 COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\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 LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\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": {
"columns": [
{
"ordinal": 0,
"name": "key",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "issue_type",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "severity: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
},
{
"ordinal": 3,
"name": "jar",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "file_path",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "data: Json<HashMap<String, serde_json::Value>>",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "delphi_version",
"type_info": "Int4"
},
{
"ordinal": 8,
"name": "size?",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "file_id?",
"type_info": "Int8"
},
{
"ordinal": 10,
"name": "version_id?",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "project_id?",
"type_info": "Int8"
},
{
"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>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
null,
false,
false,
false,
false,
false,
null,
null,
null
]
},
"hash": "448ba47493dc3b83b33ce83316c2ad488032235981bc118532ad1094701eae2d"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n -- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n\t\t\t\t\t\tdri.report_id = dr.id\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ) AS \"data!: sqlx::types::Json<FileReport>\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "data!: sqlx::types::Json<FileReport>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "48dfc2f2bcf8917f110b7b2b142167f1525cdea2ace9e6165e5c5f596cbd9fb3"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT project_id, type, metadata\n FROM project_disclosures\n WHERE project_id = ANY($1)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "type",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "metadata",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "4ccad9e07ae61ba87b1784b6ee9a136ddbe802cd3a4c649e4982d9f28ae097ed"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT requested.project_id AS \"project_id!: DBProjectId\"\n FROM unnest($1::bigint[]) AS requested(project_id)\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.project_id = requested.project_id\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
null
]
},
"hash": "4e30093d30c2ee3c39e6e3f2d9ffdc2ba5455d2213e06bc3e273f250ef97a525"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM delphi_rule_effects WHERE revision <> $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n ",
"query": "\n SELECT\n didws.id AS \"issue_detail_id!\"\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.issue_id = dri.id\n WHERE\n m.id = $1\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n ",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
true
]
},
"hash": "52ef6d02f8d533fc4e4ceb141d07a2eb115dc88da24735fffeca3eb1c269ad53"
"hash": "563d4d8ff958b3a915e6e20b0654c9f189f1c34a94c27a2b894e7eade286b8a3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
"query": "\n SELECT id\n FROM mods\n WHERE\n status = $1\n AND (\n $3::boolean = false\n OR NOT EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.project_id = mods.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n )\n ORDER BY\n CASE WHEN $2 = 'newest' THEN COALESCE(queued, published) END DESC NULLS LAST,\n CASE WHEN $2 = 'oldest' THEN COALESCE(queued, published) END ASC NULLS LAST,\n id ASC\n ",
"describe": {
"columns": [
{
@@ -20,5 +20,5 @@
false
]
},
"hash": "1bff1c5714dd039814d7a9d9f25f4ceca0e84a42b3a4c414210739fec6308e2d"
"hash": "5d5024681546a90c9fb18dae934a0f99a63c3f075cbad8545000a03a5a030323"
}
@@ -1,60 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH project_ids AS (\n SELECT unnest($1::bigint[]) AS project_id\n ),\n detail_states AS (\n SELECT\n p.project_id,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_pending_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'unsafe'\n AND dri.issue_type != $3\n ),\n FALSE\n ) AS has_unsafe_detail,\n COALESCE(\n BOOL_OR(\n didws.status = 'pending'\n AND dri.issue_type = $3\n ),\n FALSE\n ) AS has_dummy\n FROM project_ids p\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.project_id = p.project_id\n LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n GROUP BY p.project_id\n )\n SELECT\n p.project_id AS \"project_id!: DBProjectId\",\n p.has_pending_detail AS \"has_pending_detail!\",\n p.has_unsafe_detail AS \"has_unsafe_detail!\",\n p.has_dummy AS \"has_dummy!\",\n (\n SELECT t.id\n FROM threads t\n WHERE t.mod_id = p.project_id\n ORDER BY t.id\n LIMIT 1\n ) AS \"thread_id: DBThreadId\",\n (\n SELECT tm.body->>'type'\n FROM threads t\n INNER JOIN threads_messages tm ON tm.thread_id = t.id\n WHERE\n t.mod_id = p.project_id\n AND tm.body->>'type' = ANY($2::text[])\n ORDER BY tm.created DESC, tm.id DESC\n LIMIT 1\n ) AS \"last_tech_review_message_type\",\n (\n SELECT dr.id\n FROM versions v\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n WHERE v.mod_id = p.project_id\n ORDER BY dr.created DESC, dr.id DESC\n LIMIT 1\n ) AS \"report_id: DelphiReportId\"\n FROM detail_states p\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "has_pending_detail!",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "has_unsafe_detail!",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "has_dummy!",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "thread_id: DBThreadId",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "last_tech_review_message_type",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "report_id: DelphiReportId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array",
"TextArray",
"Text"
]
},
"nullable": [
null,
null,
null,
null,
null,
null,
null
]
},
"hash": "6241bc72a80de01fd8e5506442da48057c24dece008dbf2c00c6ec135d8cef41"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n m.id AS \"project_id: DBProjectId\",\n MIN(t.id) AS \"thread_id!: DBThreadId\"\n FROM mods m\n INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN files f ON f.version_id = v.id\n LEFT JOIN delphi_reports dr ON dr.file_id = f.id\n LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n LEFT JOIN threads_messages tm_last\n ON tm_last.thread_id = t.id\n AND tm_last.id = (\n SELECT id FROM threads_messages\n WHERE thread_id = t.id\n ORDER BY created DESC\n LIMIT 1\n )\n LEFT JOIN users u_last\n ON u_last.id = tm_last.author_id\n WHERE\n (\n cardinality($4::text[]) = 0\n OR (\n 'minecraft_java_server' = ANY($4::text[])\n AND (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n OR EXISTS (\n SELECT 1\n FROM versions type_v\n INNER JOIN loaders_versions type_lv\n ON type_lv.version_id = type_v.id\n INNER JOIN loaders_project_types type_lpt\n ON type_lpt.joining_loader_id = type_lv.loader_id\n INNER JOIN project_types type_pt\n ON type_pt.id = type_lpt.joining_project_type_id\n WHERE\n type_v.mod_id = m.id\n AND type_pt.name = ANY($4::text[])\n AND (\n type_pt.name != 'modpack'\n OR NOT (\n m.components ? 'minecraft_server'\n OR m.components ? 'minecraft_java_server'\n )\n )\n )\n )\n AND m.status NOT IN ('draft', 'rejected', 'withheld')\n AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))\n AND (\n cardinality($7::text[]) = 0\n OR EXISTS (\n SELECT 1\n FROM versions issue_version\n INNER JOIN files issue_file\n ON issue_file.version_id = issue_version.id\n INNER JOIN delphi_reports issue_report\n ON issue_report.file_id = issue_file.id\n INNER JOIN delphi_report_issues issue\n ON issue.report_id = issue_report.id\n INNER JOIN delphi_issue_details_with_statuses detail\n ON detail.issue_id = issue.id\n WHERE\n issue_version.mod_id = m.id\n AND issue.issue_type = ANY($7::text[])\n AND detail.severity != 'hidden'\n )\n )\n AND (\n $5::text IS NULL\n OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))\n OR ($5::text = 'replied' AND tm_last.id IS NOT NULL AND u_last.role IS NOT NULL AND u_last.role IN ('moderator', 'admin'))\n )\n GROUP BY m.id\n ORDER BY\n CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,\n CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,\n CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC,\n CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC,\n -- tie-breaker: oldest reports\n MIN(dr.created) ASC\n LIMIT $1 OFFSET $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "thread_id!: DBThreadId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Int8",
"Text",
"TextArray",
"Text",
"TextArray",
"TextArray"
]
},
"nullable": [
false,
null
]
},
"hash": "628d178e568998627de7e7cdba3b5c83300b5649f2ccc215f20fd82e258e76f4"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT pg_try_advisory_xact_lock($1)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "pg_try_advisory_xact_lock",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "6776dc50f184188756ad7fe263b0304333536768527525a43bdd45aedffa3c4f"
}
@@ -0,0 +1,124 @@
{
"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 COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\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 LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\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": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "key",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "issue_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "severity: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
},
{
"ordinal": 4,
"name": "jar",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "file_path",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "data: Json<HashMap<String, serde_json::Value>>",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "delphi_version",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "size?",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "file_id?",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "version_id?",
"type_info": "Int8"
},
{
"ordinal": 12,
"name": "project_id?",
"type_info": "Int8"
},
{
"ordinal": 13,
"name": "project_types!: Vec<String>",
"type_info": "TextArray"
},
{
"ordinal": 14,
"name": "loaders!: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 15,
"name": "hashes!: Json<BTreeMap<String, String>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
true,
false,
false,
null,
false,
false,
false,
false,
false,
null,
null,
null
]
},
"hash": "6cbc61717a4ac5dd6d1b07137643526bf321a861f42b59c9b21c6f50196d3158"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.key = ANY($1::text[])\n ",
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.id = ANY($1::bigint[])\n ",
"describe": {
"columns": [
{
@@ -11,12 +11,12 @@
],
"parameters": {
"Left": [
"TextArray"
"Int8Array"
]
},
"nullable": [
true
]
},
"hash": "ba7585f55df2a4596caae265ac956eb23268f26aef664234b898b17d13c09f91"
"hash": "6fa83172261e64445f2fa18d2c98eb68f2617f6f57a31edda36c2f861e7da7f7"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "LOCK TABLE delphi_rules IN SHARE MODE",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT ON (dr.id)\n to_jsonb(dr)\n || jsonb_build_object(\n 'report_id', dr.id,\n 'file_id', to_base62(f.id),\n 'version_id', to_base62(v.id),\n 'project_id', to_base62(v.mod_id),\n 'file_name', f.filename,\n 'file_size', f.size,\n 'flag_reason', 'delphi',\n 'download_url', f.url,\n 'severity', COALESCE((\n SELECT MAX(didws.severity)\n FROM delphi_report_issues severity_issue\n INNER JOIN delphi_issue_details_with_statuses didws\n ON didws.issue_id = severity_issue.id\n WHERE\n severity_issue.report_id = dr.id\n AND didws.severity != 'hidden'\n ), 'low'::delphi_severity),\n -- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t'issues', (\n\t\t\t\t\tSELECT coalesce(json_agg(\n\t\t\t\t\t\tto_jsonb(dri)\n\t\t\t\t\t\t|| jsonb_build_object(\n\t\t\t\t\t\t\t-- TODO: replace with `json_array` in Postgres 16\n\t\t\t\t\t\t\t'details', (\n\t\t\t\t\t\t\t\tSELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n )\n\t\t\t\t\t\t)\n\t\t\t\t\t), '[]'::json)\n\t\t\t\t\tFROM delphi_report_issues dri\n\t\t\t\t\tWHERE\n dri.report_id = dr.id\n AND EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses visible_detail\n WHERE\n visible_detail.issue_id = dri.id\n AND visible_detail.severity != 'hidden'\n )\n )\n ) AS \"data!: sqlx::types::Json<FileReport>\"\n FROM delphi_reports dr\n INNER JOIN files f ON f.id = dr.file_id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE dr.id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "data!: sqlx::types::Json<FileReport>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
null
]
},
"hash": "852eff37ff36e1e87fcbc6c50f27bab003a40537c191eb6cb64a01d92b57e25f"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tSELECT project_id\n\t\t\tFROM project_disclosures\n\t\t\tWHERE type = $1 AND project_id = ANY($2)\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Int8Array"
]
},
"nullable": [
false
]
},
"hash": "87d4778e52ac3e9fd885f9c77595705f8efc09ac21e0cf9a15c4cce019aaeb05"
}
@@ -0,0 +1,38 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO delphi_rule_effects (\n revision,\n detail_id,\n rule_id,\n severity\n )\n SELECT $1, effect.*\n FROM UNNEST(\n $2::BIGINT[],\n $3::BIGINT[],\n $4::delphi_severity[]\n ) AS effect(detail_id, rule_id, severity)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Int8Array",
"Int8Array",
{
"Custom": {
"name": "delphi_severity[]",
"kind": {
"Array": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
}
}
}
]
},
"nullable": []
},
"hash": "8c7f17ad005b42b4c6cac5ddb6a1f2d13592cc6995d1b87e79397ab592ad7675"
}
@@ -26,10 +26,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -22,10 +22,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM delphi_report_issue_details drid\n WHERE issue_id IN (\n SELECT dri.id\n FROM mods m\n INNER JOIN versions v ON v.mod_id = m.id\n INNER JOIN files f ON f.version_id = v.id\n INNER JOIN delphi_reports dr ON dr.file_id = f.id\n INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id\n WHERE m.id = $1 AND dri.issue_type = '__dummy'\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "9ab1f07c2968b5d445752c1480345c1fa3af3a899b232482aab9cc44b9336063"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT m.id\n FROM mods m\n WHERE\n EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.project_id = m.id\n AND didws.status = 'pending'\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n AND NOT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n -- see delphi.rs todo comment\n AND dri.issue_type != '__dummy'\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "9b0edb6399bc5f38ee96677cb2539476ecf08549b914f96f0c23fbfbe3f17f2e"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE didws.issue_id = dri.id\n )\n ) AS \"data!: sqlx::types::Json<FileIssue>\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ",
"query": "\n SELECT\n to_jsonb(dri)\n || jsonb_build_object(\n -- TODO: replace with `json_array` in Postgres 16\n 'details', (\n SELECT coalesce(jsonb_agg(\n jsonb_build_object(\n 'id', didws.id,\n 'issue_id', didws.issue_id,\n 'key', didws.key,\n 'file_path', didws.file_path,\n 'decompiled_source', didws.decompiled_source,\n 'data', didws.data,\n 'severity', didws.severity,\n 'local_status', didws.local_status,\n 'global_status', didws.global_status,\n 'status', didws.status\n )\n ), '[]'::jsonb)\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.issue_id = dri.id\n AND didws.severity != 'hidden'\n )\n ) AS \"data!: sqlx::types::Json<FileIssue>\"\n FROM delphi_report_issues dri\n WHERE dri.id = $1\n ",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "c3598ed9f64f7151b83d47a15fabfdcf015bda1b3c0a7f2358c7f284feb532c3"
"hash": "9d044826831bd15b1e0772e8a5db312189b48fcafaaf1b8c4c9f483c97b694ed"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tDELETE FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = $2\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "9eea063cb153da407548f957cef7da1a33a059b0a1d00b51e27b4a5399492b13"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE detail.key = ANY($1::text[])\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": [
true
]
},
"hash": "a1f38a33cc3159108dd7deda48a60ebdae8d60a6d555a625ee2ec5780ab78fc3"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT m.id\n FROM mods m\n INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id\n WHERE\n EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status = 'pending'\n AND didws.severity != 'hidden'\n )\n AND NOT EXISTS(\n SELECT 1\n FROM delphi_issue_details_with_statuses didws\n WHERE\n didws.project_id = m.id\n AND didws.status IN ('safe', 'unsafe')\n AND didws.severity != 'hidden'\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "a58e701218270abb1c5b177d7ab12a8d3fce511a3d03e883de8ce925f3332957"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE delphi_rule_revisions SET revision = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT revision FROM delphi_rule_revisions LIMIT 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "revision",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "abef4603febf18dae62ca424b0433f2bacc2555433277ee195ed6fbedb7be537"
}
@@ -0,0 +1,73 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tINSERT INTO delphi_rules (\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t)\n\t\tVALUES (\n\t\t\t$1,\n\t\t\t$2,\n\t\t\t$3,\n\t\t\t(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1),\n\t\t\t$4,\n\t\t\t$4\n\t\t)\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: DelphiRuleId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "rule",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "priority",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "revision",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Int8"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Int4",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "b0004df8d3b1c5f9193a21f981452ce4ae56f608ec698f6f1ebb4c470bb2f0f5"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tdelete_on_next_revision = TRUE,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $2\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING id\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "b5233d5b729fae87fdf24e7d5f9ee6c96fe5a33015ba94bc2c606c31f87af9c9"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n -- see delphi.rs todo comment\n dri.issue_type != '__dummy'\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "all_found!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "upserted_count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array",
"TextArray"
]
},
"nullable": [
null,
null
]
},
"hash": "c40c2085202bc4486568fb24db9201239cefbe174f4a5cc4b8eb21a10268cd73"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT detail.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses detail\n WHERE\n detail.project_id = ANY($1::bigint[])\n AND detail.status IN ('pending', 'unsafe')\n AND detail.severity != 'hidden'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
true
]
},
"hash": "cb983aed301378550831204ff7f88e95e5db9cabd8e1fc177cdfdaf0951e58e4"
}
@@ -1,19 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tINSERT INTO project_disclosures (project_id, type, metadata, updated_by, set_by_moderator, lock_status)\n\t\t\tVALUES ($1, $2, $3, $4, $5, $6)\n\t\t\tON CONFLICT (project_id, type) DO UPDATE SET\n\t\t\t\tmetadata = $3,\n\t\t\t\tupdated_at = now(),\n\t\t\t\tupdated_by = $4,\n\t\t\t\tset_by_moderator = $5,\n\t\t\t\tlock_status = $6\n\t\t\t",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text",
"Jsonb",
"Int8",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "d4b427672138b0c75f123a23ce104f69a6d12ff29e21a15f8fdd73386634a3ec"
}
@@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH removed AS (\n DELETE FROM delphi_tech_review_queue\n WHERE project_id = ANY($1::bigint[])\n RETURNING project_id\n )\n SELECT\n removed.project_id AS \"project_id!: DBProjectId\",\n (\n SELECT thread.id\n FROM threads thread\n WHERE thread.mod_id = removed.project_id\n ORDER BY thread.id\n LIMIT 1\n ) AS \"thread_id!: DBThreadId\"\n FROM removed\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "thread_id!: DBThreadId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
false,
null
]
},
"hash": "d4d04f7cfd6efa582970ec0636f16df646df14ec647f7f2017d59fe786b9ad5b"
}
@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH incoming AS (\n SELECT *\n FROM unnest($1::bigint[], $2::text[]) WITH ORDINALITY\n AS u(detail_id, verdict, ord)\n ),\n resolved AS (\n SELECT\n i.ord,\n didws.project_id,\n didws.key AS detail_key,\n i.verdict\n FROM incoming i\n INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id\n ),\n validated AS (\n SELECT\n (SELECT COUNT(*) FROM incoming) AS incoming_count,\n (SELECT COUNT(*) FROM resolved) AS resolved_count\n ),\n latest AS (\n SELECT DISTINCT ON (project_id, detail_key)\n project_id,\n detail_key,\n verdict\n FROM resolved\n ORDER BY project_id, detail_key, ord DESC\n ),\n deleted AS (\n DELETE FROM delphi_issue_detail_verdicts didv\n USING latest\n WHERE\n didv.project_id = latest.project_id\n AND didv.detail_key = latest.detail_key\n AND latest.verdict = 'pending'\n RETURNING 1\n ),\n upserted AS (\n INSERT INTO delphi_issue_detail_verdicts (\n project_id,\n detail_key,\n verdict,\n updated_at\n )\n SELECT\n project_id,\n detail_key,\n verdict::delphi_report_issue_status,\n NOW()\n FROM latest\n WHERE verdict != 'pending'\n ON CONFLICT (project_id, detail_key)\n DO UPDATE SET\n verdict = EXCLUDED.verdict,\n updated_at = EXCLUDED.updated_at\n RETURNING 1\n )\n SELECT\n (v.incoming_count = v.resolved_count) AS \"all_found!\",\n (SELECT COUNT(*) FROM upserted) AS \"upserted_count!\"\n FROM validated v\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "all_found!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "upserted_count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array",
"TextArray"
]
},
"nullable": [
null,
null
]
},
"hash": "d7dd31de696eeb696a692787c3ee17d6bc343a6537944a82fdff4f94d82cdc3e"
}
@@ -0,0 +1,74 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tUPDATE delphi_rules\n\t\tSET\n\t\t\tname = $2,\n\t\t\trule = $3,\n\t\t\tpriority = $4,\n\t\t\trevision = (\n\t\t\t\tSELECT revision + 1 FROM delphi_rule_revisions LIMIT 1\n\t\t\t),\n\t\t\tupdated_at = CURRENT_TIMESTAMP,\n\t\t\tupdated_by = $5\n\t\tWHERE id = $1 AND NOT delete_on_next_revision\n\t\tRETURNING\n\t\t\tid AS \"id!: DelphiRuleId\",\n\t\t\tname,\n\t\t\trule,\n\t\t\tpriority,\n\t\t\trevision,\n\t\t\tcreated_at,\n\t\t\tupdated_at,\n\t\t\tcreated_by,\n\t\t\tupdated_by\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!: DelphiRuleId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "rule",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "priority",
"type_info": "Int4"
},
{
"ordinal": 4,
"name": "revision",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 7,
"name": "created_by",
"type_info": "Int8"
},
{
"ordinal": 8,
"name": "updated_by",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Varchar",
"Int4",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
true,
true
]
},
"hash": "d973a1edd200b4cd68ec30556a1e12240885f2372265597efa6e0361831d1318"
}
@@ -1,58 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\t\tSELECT project_id, type AS \"disclosure_type!\", metadata, updated_at, updated_by, set_by_moderator, lock_status\n\t\t\tFROM project_disclosures\n\t\t\tWHERE project_id = $1\n\t\t\tORDER BY updated_at DESC\n\t\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "disclosure_type!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "metadata",
"type_info": "Jsonb"
},
{
"ordinal": 3,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 4,
"name": "updated_by",
"type_info": "Int8"
},
{
"ordinal": 5,
"name": "set_by_moderator",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "lock_status",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "dc13b4d8b84681dc4f1ea5f4f735a5176583d2781dbb9aa428f8f920844f2a1e"
}
@@ -0,0 +1,128 @@
{
"db_name": "PostgreSQL",
"query": "\n\t\tSELECT\n\t\t\teffect.detail_id AS \"detail_id!: DelphiReportIssueDetailsId\",\n\t\t\tdetail.issue_id AS \"issue_id!: DelphiReportIssueId\",\n\t\t\tversion.mod_id AS \"project_id?: DBProjectId\",\n\t\t\tproject.name AS \"project_name?\",\n\t\t\tproject.icon_url AS \"project_icon_url?\",\n\t\t\tversion.id AS \"version_id?: DBVersionId\",\n\t\t\tversion.name AS \"version_name?\",\n\t\t\tversion.version_number AS \"version_number?\",\n\t\t\tissue.issue_type,\n\t\t\tdetail.key,\n\t\t\tdetail.jar,\n\t\t\tdetail.file_path,\n\t\t\tdetail.severity AS \"original_severity!: DelphiSeverity\",\n\t\t\teffect.severity AS \"effect_severity!: DelphiSeverity\"\n\t\tFROM delphi_rule_effects effect\n\t\tINNER JOIN delphi_rule_revisions published\n\t\t\tON published.revision = effect.revision\n\t\tINNER JOIN delphi_report_issue_details detail\n\t\t\tON detail.id = effect.detail_id\n\t\tINNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id\n\t\tINNER JOIN delphi_reports report ON report.id = issue.report_id\n\t\tLEFT JOIN files file ON file.id = report.file_id\n\t\tLEFT JOIN versions version ON version.id = file.version_id\n\t\tLEFT JOIN mods project ON project.id = version.mod_id\n\t\tWHERE effect.rule_id = $1\n\t\tORDER BY effect.detail_id DESC\n\t\t",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "detail_id!: DelphiReportIssueDetailsId",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "issue_id!: DelphiReportIssueId",
"type_info": "Int8"
},
{
"ordinal": 2,
"name": "project_id?: DBProjectId",
"type_info": "Int8"
},
{
"ordinal": 3,
"name": "project_name?",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "project_icon_url?",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "version_id?: DBVersionId",
"type_info": "Int8"
},
{
"ordinal": 6,
"name": "version_name?",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "version_number?",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "issue_type",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "key",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "jar",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "file_path",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "original_severity!: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
},
{
"ordinal": 13,
"name": "effect_severity!: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true,
false,
false,
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "e07b7f513dd5d2a0e06cc1880746cd8d7d4c27896cbfcb3e26085f99e3552c98"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT DISTINCT didws.project_id AS \"project_id!: DBProjectId\"\n FROM delphi_issue_details_with_statuses didws\n INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id\n WHERE\n didws.id = ANY($1::bigint[])\n AND dri.issue_type != '__dummy'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "project_id!: DBProjectId",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
true
]
},
"hash": "e3c356bd41074ba4b3474dfab5cc0349636abea5b21faf5fc18d9f69fd21db1d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b"
}
@@ -0,0 +1,126 @@
{
"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 COALESCE(sibling_traces.traces, '[]'::jsonb)\n AS \"sibling_traces!: Json<Vec<RuleTrace>>\",\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 LATERAL (\n SELECT\n jsonb_agg(\n jsonb_build_object(\n 'key', sibling_detail.key,\n 'issue_type', sibling_issue.issue_type,\n 'severity', sibling_detail.severity,\n 'jar', sibling_detail.jar,\n 'file_path', sibling_detail.file_path,\n 'data', sibling_detail.data\n )\n ORDER BY sibling_detail.id\n ) AS traces\n FROM delphi_report_issues sibling_issue\n INNER JOIN delphi_report_issue_details sibling_detail\n ON sibling_detail.issue_id = sibling_issue.id\n WHERE sibling_issue.report_id = issue.report_id\n AND sibling_detail.id != detail.id\n ) sibling_traces ON TRUE\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": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "key",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "issue_type",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "severity: DelphiSeverity",
"type_info": {
"Custom": {
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe",
"malware"
]
}
}
}
},
{
"ordinal": 4,
"name": "jar",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "file_path",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "data: Json<HashMap<String, serde_json::Value>>",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "sibling_traces!: Json<Vec<RuleTrace>>",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "delphi_version",
"type_info": "Int4"
},
{
"ordinal": 9,
"name": "size?",
"type_info": "Int4"
},
{
"ordinal": 10,
"name": "file_id?",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "version_id?",
"type_info": "Int8"
},
{
"ordinal": 12,
"name": "project_id?",
"type_info": "Int8"
},
{
"ordinal": 13,
"name": "project_types!: Vec<String>",
"type_info": "TextArray"
},
{
"ordinal": 14,
"name": "loaders!: Vec<String>",
"type_info": "VarcharArray"
},
{
"ordinal": 15,
"name": "hashes!: Json<BTreeMap<String, String>>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8Array"
]
},
"nullable": [
false,
false,
false,
false,
true,
false,
false,
null,
false,
true,
true,
true,
true,
null,
null,
null
]
},
"hash": "e8ea2268b7d3e4ec1741bfc0312306aad5e2d79f1bb4bf144d48933f7fa1ee83"
}
@@ -19,10 +19,12 @@
"name": "delphi_severity",
"kind": {
"Enum": [
"hidden",
"low",
"medium",
"high",
"severe"
"severe",
"malware"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ",
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE dgdv.detail_key = $1\n GROUP BY dgdv.detail_key, dgdv.verdict\n ",
"describe": {
"columns": [
{
@@ -41,5 +41,5 @@
null
]
},
"hash": "5e5fcfa6ac68f296ba5238e8211f8e206473f11fe1796fc2feda15f8d0ee7f41"
"hash": "f952ab269d0aa1b98bb86b58debfd4ab14cea8575e4a15cf8c1c57e9c1e367a1"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH dummy_issue AS (\n INSERT INTO delphi_report_issues (report_id, issue_type)\n VALUES ($1, $2)\n ON CONFLICT (report_id, issue_type)\n DO UPDATE SET issue_type = EXCLUDED.issue_type\n RETURNING id\n )\n INSERT INTO delphi_report_issue_details (\n issue_id,\n key,\n jar,\n file_path,\n decompiled_source,\n data,\n severity\n )\n SELECT\n id,\n '',\n NULL,\n '',\n NULL,\n '{}'::jsonb,\n 'low'::delphi_severity\n FROM dummy_issue\n WHERE NOT EXISTS (\n SELECT 1\n FROM delphi_report_issue_details drid\n WHERE drid.issue_id = dummy_issue.id\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "fc6c20348de487e4039ee7d53212ecd1856bde7aa4be6e366c98cef1321a7279"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n AND dri.issue_type != '__dummy'\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ",
"query": "\n SELECT\n dgdv.detail_key,\n dgdv.verdict AS \"verdict!: DelphiStatus\",\n COUNT(dri.id) AS \"local_trace_count!\"\n FROM delphi_global_detail_verdicts dgdv\n LEFT JOIN delphi_issue_details_with_statuses didws\n ON didws.key = dgdv.detail_key\n AND didws.severity != 'hidden'\n LEFT JOIN delphi_report_issues dri\n ON dri.id = didws.issue_id\n WHERE (\n $1::text IS NULL\n OR dgdv.detail_key ILIKE '%' || $1 || '%'\n OR EXISTS (\n SELECT 1\n FROM delphi_issue_details_with_statuses matching_didws\n INNER JOIN delphi_report_issues matching_dri\n ON matching_dri.id = matching_didws.issue_id\n WHERE\n matching_didws.key = dgdv.detail_key\n AND matching_dri.issue_type != '__dummy'\n AND matching_didws.file_path ILIKE '%' || $1 || '%'\n )\n )\n GROUP BY dgdv.detail_key, dgdv.verdict\n ORDER BY dgdv.detail_key\n LIMIT $2 OFFSET $3\n ",
"describe": {
"columns": [
{
@@ -43,5 +43,5 @@
null
]
},
"hash": "33d4e31565ece4a99832cbfd0a39bc49666e9fc8a400a5c41660504205e7cac5"
"hash": "ffca905cc54942581efa461115fac2260bbc8d593aab70a2510491724981d56d"
}
+1
View File
@@ -33,6 +33,7 @@ aws-sdk-s3 = { workspace = true }
base64 = { workspace = true }
bitflags = { workspace = true }
bytes = { workspace = true }
cel = { workspace = true }
censor = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
chumsky = { workspace = true }
@@ -0,0 +1,342 @@
-- Development fixtures for the Delphi trace-rule editor and scanner.
-- This file is idempotent and only replaces rows using the fixture namespace.
BEGIN;
DELETE FROM delphi_rule_effects
WHERE rule_id IN (
SELECT id
FROM delphi_rules
WHERE name LIKE '[DEV trace-rule fixture]%'
);
DELETE FROM delphi_reports
WHERE artifact_url = 'https://example.invalid/fixtures/delphi-trace-rules.json';
DELETE FROM delphi_rules
WHERE name LIKE '[DEV trace-rule fixture]%';
WITH fixture_report AS (
INSERT INTO delphi_reports (
file_id,
delphi_version,
artifact_url,
severity
)
VALUES (
NULL,
18,
'https://example.invalid/fixtures/delphi-trace-rules.json',
'malware'
)
RETURNING id
), fixture_issues AS (
INSERT INTO delphi_report_issues (report_id, issue_type)
SELECT fixture_report.id, fixture_issue.issue_type
FROM fixture_report
CROSS JOIN (
VALUES
('OBFUSCATED_NAMES'),
('SUSPICIOUS_NETWORK_ACCESS'),
('RUNTIME_EXEC_USAGE'),
('BUNDLED_LIBRARY'),
('HARDCODED_URL'),
('CRYPTO_MINING_SIGNATURE'),
('CLASSLOADER_USAGE'),
('NATIVE_LIBRARY_LOAD')
) AS fixture_issue(issue_type)
RETURNING id, issue_type
)
INSERT INTO delphi_report_issue_details (
issue_id,
key,
jar,
file_path,
decompiled_source,
data,
severity
)
SELECT
fixture_issues.id,
fixture_detail.key,
fixture_detail.jar,
fixture_detail.file_path,
fixture_detail.decompiled_source,
fixture_detail.data,
fixture_detail.severity::delphi_severity
FROM fixture_issues
INNER JOIN (
VALUES
(
'OBFUSCATED_NAMES',
'dev-trace-rules/obfuscation/known-bootstrap',
'META-INF/jars/bootstrap.jar',
'com/example/bootstrap/Bootstrap.class',
'class Bootstrap { /* generated fixture */ }',
'{"confidence": 0.99, "symbol_count": 58}'::jsonb,
'high'
),
(
'OBFUSCATED_NAMES',
'dev-trace-rules/obfuscation/unknown-library',
NULL,
'com/example/internal/A.class',
'class A { /* generated fixture */ }',
'{"confidence": 0.71, "symbol_count": 240}'::jsonb,
'high'
),
(
'SUSPICIOUS_NETWORK_ACCESS',
'dev-trace-rules/network/malware-host',
NULL,
'com/example/network/Backdoor.class',
'class Backdoor { /* generated fixture */ }',
'{"host": "evil.example", "port": 4444, "protocol": "tcp"}'::jsonb,
'medium'
),
(
'SUSPICIOUS_NETWORK_ACCESS',
'dev-trace-rules/network/telemetry-host',
NULL,
'com/example/telemetry/TelemetryClient.class',
'class TelemetryClient { /* generated fixture */ }',
'{"host": "telemetry.example.com", "port": 443, "protocol": "https"}'::jsonb,
'medium'
),
(
'SUSPICIOUS_NETWORK_ACCESS',
'dev-trace-rules/network/unmatched-host',
NULL,
'com/example/update/UpdateChecker.class',
'class UpdateChecker { /* generated fixture */ }',
'{"host": "api.github.com", "port": 443, "protocol": "https"}'::jsonb,
'low'
),
(
'RUNTIME_EXEC_USAGE',
'dev-trace-rules/runtime/powershell',
NULL,
'com/example/platform/WindowsHelper.class',
'class WindowsHelper { /* generated fixture */ }',
'{"command": "powershell.exe", "arguments": ["-EncodedCommand", "fixture"]}'::jsonb,
'high'
),
(
'RUNTIME_EXEC_USAGE',
'dev-trace-rules/runtime/desktop-opener',
NULL,
'com/example/platform/DesktopBrowser.class',
'class DesktopBrowser { /* generated fixture */ }',
'{"command": "xdg-open", "arguments": ["https://example.com/docs"]}'::jsonb,
'medium'
),
(
'RUNTIME_EXEC_USAGE',
'dev-trace-rules/runtime/unmatched-java',
NULL,
'com/example/launcher/JavaLauncher.class',
'class JavaLauncher { /* generated fixture */ }',
'{"command": "java", "arguments": ["-version"]}'::jsonb,
'low'
),
(
'BUNDLED_LIBRARY',
'dev-trace-rules/library/example-helper',
'META-INF/jars/example-helper.jar',
'com/example/helper/Helper.class',
NULL,
'{"library": "example-helper", "version": "1.0.0"}'::jsonb,
'high'
),
(
'BUNDLED_LIBRARY',
'dev-trace-rules/library/kotlin-runtime',
'META-INF/jars/kotlin-stdlib.jar',
'kotlin/collections/CollectionsKt.class',
NULL,
'{"library": "kotlin-stdlib", "version": "2.1.0"}'::jsonb,
'medium'
),
(
'HARDCODED_URL',
'dev-trace-rules/url/known-documentation',
NULL,
'com/example/help/DocumentationLink.class',
'class DocumentationLink { /* generated fixture */ }',
'{"url": "https://docs.example.com/help"}'::jsonb,
'low'
),
(
'HARDCODED_URL',
'dev-trace-rules/url/unmatched-webhook',
NULL,
'com/example/integration/WebhookClient.class',
'class WebhookClient { /* generated fixture */ }',
'{"url": "https://hooks.example.net/incoming"}'::jsonb,
'medium'
),
(
'CRYPTO_MINING_SIGNATURE',
'dev-trace-rules/crypto/randomx-miner',
NULL,
'com/example/worker/NativeWorker.class',
'class NativeWorker { /* generated fixture */ }',
'{"algorithm": "randomx", "pool": "pool.example"}'::jsonb,
'severe'
),
(
'CLASSLOADER_USAGE',
'dev-trace-rules/classloader/plugin-loader',
'META-INF/jars/plugin-api.jar',
'com/example/plugin/PluginLoader.class',
'class PluginLoader { /* generated fixture */ }',
'{"purpose": "plugin-discovery", "parent_first": true}'::jsonb,
'high'
),
(
'NATIVE_LIBRARY_LOAD',
'dev-trace-rules/native/unmatched-jna',
'META-INF/jars/jna.jar',
'com/sun/jna/Native.class',
NULL,
'{"library": "jnidispatch", "platform": "linux-x86_64"}'::jsonb,
'high'
)
) AS fixture_detail(
issue_type,
key,
jar,
file_path,
decompiled_source,
data,
severity
) USING (issue_type);
INSERT INTO delphi_rules (name, rule, priority, revision)
VALUES
(
'[DEV trace-rule fixture] Escalate known malware host',
'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "evil.example" ? "malware" : null',
1000,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Escalate crypto-mining signatures',
'trace.issue_type == "CRYPTO_MINING_SIGNATURE" && trace.data.algorithm == "randomx" ? {"severity": "malware"} : null',
900,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Escalate encoded PowerShell',
'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "powershell.exe" ? {"severity": "severe"} : null',
800,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Hide known-safe obfuscation',
'trace.issue_type == "OBFUSCATED_NAMES" && trace.data.confidence >= 0.95 ? "hidden" : null',
700,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Downgrade known telemetry',
'trace.issue_type == "SUSPICIOUS_NETWORK_ACCESS" && trace.data.host == "telemetry.example.com" ? {"severity": "low"} : null',
600,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Downgrade desktop URL openers',
'trace.issue_type == "RUNTIME_EXEC_USAGE" && trace.data.command == "xdg-open" ? {"severity": "low"} : null',
500,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Downgrade bundled libraries',
'trace.issue_type == "BUNDLED_LIBRARY" ? "low" : null',
400,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Hide documentation URLs',
'trace.issue_type == "HARDCODED_URL" && trace.data.url == "https://docs.example.com/help" ? {"severity": "hidden"} : null',
300,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Downgrade plugin classloaders',
'trace.issue_type == "CLASSLOADER_USAGE" && trace.data.purpose == "plugin-discovery" ? {"severity": "low"} : null',
200,
(SELECT revision FROM delphi_rule_revisions LIMIT 1)
),
(
'[DEV trace-rule fixture] Pending native-library exception',
'trace.issue_type == "NATIVE_LIBRARY_LOAD" && trace.data.library == "jnidispatch" ? {"severity": "low"} : null',
100,
(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1)
);
INSERT INTO delphi_rule_effects (revision, detail_id, rule_id, severity)
SELECT
(SELECT revision FROM delphi_rule_revisions LIMIT 1),
detail.id,
rule.id,
fixture_effect.severity::delphi_severity
FROM (
VALUES
(
'dev-trace-rules/network/malware-host',
'[DEV trace-rule fixture] Escalate known malware host',
'malware'
),
(
'dev-trace-rules/crypto/randomx-miner',
'[DEV trace-rule fixture] Escalate crypto-mining signatures',
'malware'
),
(
'dev-trace-rules/runtime/powershell',
'[DEV trace-rule fixture] Escalate encoded PowerShell',
'severe'
),
(
'dev-trace-rules/obfuscation/known-bootstrap',
'[DEV trace-rule fixture] Hide known-safe obfuscation',
'hidden'
),
(
'dev-trace-rules/network/telemetry-host',
'[DEV trace-rule fixture] Downgrade known telemetry',
'low'
),
(
'dev-trace-rules/runtime/desktop-opener',
'[DEV trace-rule fixture] Downgrade desktop URL openers',
'low'
),
(
'dev-trace-rules/library/example-helper',
'[DEV trace-rule fixture] Downgrade bundled libraries',
'low'
),
(
'dev-trace-rules/library/kotlin-runtime',
'[DEV trace-rule fixture] Downgrade bundled libraries',
'low'
),
(
'dev-trace-rules/url/known-documentation',
'[DEV trace-rule fixture] Hide documentation URLs',
'hidden'
),
(
'dev-trace-rules/classloader/plugin-loader',
'[DEV trace-rule fixture] Downgrade plugin classloaders',
'low'
)
) AS fixture_effect(detail_key, rule_name, severity)
INNER JOIN delphi_report_issue_details detail
ON detail.key = fixture_effect.detail_key
INNER JOIN delphi_rules rule
ON rule.name = fixture_effect.rule_name;
COMMIT;
@@ -0,0 +1,103 @@
ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'hidden' BEFORE 'low';
ALTER TYPE delphi_severity ADD VALUE IF NOT EXISTS 'malware' AFTER 'severe';
CREATE TABLE delphi_rules (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by BIGINT REFERENCES users(id)
ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_by BIGINT REFERENCES users(id)
ON DELETE SET NULL,
name VARCHAR(256) NOT NULL,
rule VARCHAR(65536) NOT NULL,
priority INTEGER NOT NULL DEFAULT 0,
revision BIGINT NOT NULL DEFAULT 0,
delete_on_next_revision BOOL NOT NULL DEFAULT FALSE
);
CREATE TABLE delphi_rule_revisions (
revision BIGINT PRIMARY KEY
);
INSERT INTO delphi_rule_revisions (revision)
VALUES (1);
CREATE TABLE delphi_rule_effects (
revision BIGINT NOT NULL,
detail_id BIGINT NOT NULL REFERENCES delphi_report_issue_details(id)
ON DELETE CASCADE
ON UPDATE CASCADE,
rule_id BIGINT NOT NULL REFERENCES delphi_rules(id)
ON UPDATE CASCADE,
severity delphi_severity NOT NULL,
PRIMARY KEY (revision, detail_id)
);
CREATE INDEX delphi_rule_effects_rule_id ON delphi_rule_effects(rule_id);
DROP VIEW delphi_issue_details_with_statuses;
CREATE VIEW delphi_issue_details_with_statuses AS
SELECT
drid.id,
drid.issue_id,
drid.key,
drid.jar,
drid.file_path,
drid.decompiled_source,
drid.data,
drid.severity AS original_severity,
COALESCE(dre.severity, drid.severity) AS severity,
m.id AS project_id,
didv.verdict AS local_status,
dgdv.verdict AS global_status,
COALESCE(dgdv.verdict, didv.verdict, 'pending') AS status
FROM delphi_report_issue_details drid
INNER JOIN delphi_report_issues dri ON dri.id = drid.issue_id
INNER JOIN delphi_reports dr ON dr.id = dri.report_id
INNER JOIN files f ON f.id = dr.file_id
INNER JOIN versions v ON v.id = f.version_id
INNER JOIN mods m ON m.id = v.mod_id
LEFT JOIN delphi_global_detail_verdicts dgdv
ON drid.key = dgdv.detail_key
LEFT JOIN delphi_issue_detail_verdicts didv
ON m.id = didv.project_id
AND drid.key = didv.detail_key
LEFT JOIN (
SELECT revision
FROM delphi_rule_revisions
LIMIT 1
) drr ON TRUE
LEFT JOIN delphi_rule_effects dre
ON dre.revision = drr.revision
AND dre.detail_id = drid.id;
CREATE TABLE delphi_tech_review_queue (
project_id BIGINT PRIMARY KEY REFERENCES mods(id)
ON DELETE CASCADE
);
INSERT INTO delphi_tech_review_queue (project_id)
SELECT DISTINCT didws.project_id
FROM delphi_issue_details_with_statuses didws
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
WHERE
(
dri.issue_type = '__dummy'
AND didws.status = 'pending'
)
OR (
dri.issue_type != '__dummy'
AND didws.status IN ('pending', 'unsafe')
AND didws.severity::text != 'hidden'
);
DELETE FROM delphi_report_issue_details detail
USING delphi_report_issues issue
WHERE
detail.issue_id = issue.id
AND issue.issue_type = '__dummy';
DELETE FROM delphi_report_issues
WHERE issue_type = '__dummy';
@@ -55,7 +55,7 @@ impl DBDelphiReport {
}
}
/// A severity level reported by Delphi.
/// A severity level for a Delphi report or transformed issue detail.
#[derive(
Deserialize,
Serialize,
@@ -76,6 +76,8 @@ impl DBDelphiReport {
#[serde(rename_all = "snake_case")]
#[sqlx(type_name = "delphi_severity", rename_all = "snake_case")]
pub enum DelphiSeverity {
#[serde(alias = "HIDDEN")]
Hidden,
#[serde(alias = "LOW")]
Low,
#[serde(alias = "MEDIUM")]
+1
View File
@@ -292,4 +292,5 @@ id_type!(StatusId as i32);
id_type!(DelphiReportId as i64);
id_type!(DelphiReportIssueId as i64);
id_type!(DelphiReportIssueDetailsId as i64);
id_type!(DelphiRuleId as i64);
id_type!(SharedInstanceId as i64);
@@ -31,7 +31,7 @@ use crate::{
};
pub mod rescan;
pub mod tech_review_sync;
pub mod tech_review_queue;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(
@@ -209,6 +209,7 @@ async fn ingest_report_deserialized(
"Delphi found issues in file",
);
let mut inserted_detail_ids = Vec::new();
for (issue_type, issue_details) in report.issues {
let issue_id = DBDelphiReportIssue {
id: DelphiReportIssueId(0), // This will be set by the database
@@ -228,7 +229,7 @@ async fn ingest_report_deserialized(
let decompiled_source =
report.decompiled_sources.get(&issue_detail.file);
ReportIssueDetail {
let detail_id = ReportIssueDetail {
id: DelphiReportIssueDetailsId(0), // This will be set by the database
issue_id,
key: issue_detail.key.0,
@@ -244,12 +245,19 @@ async fn ingest_report_deserialized(
.insert(&mut transaction)
.await
.wrap_internal_err("failed to insert Delphi issue detail")?;
inserted_detail_ids.push(detail_id);
}
}
tech_review_sync::sync_project_tech_review_state(
crate::routes::internal::moderation::tech_review::rules_scan::materialize_current_rule_effects(
&inserted_detail_ids,
&mut transaction,
)
.await
.wrap_internal_err("failed to apply delphi rules to new issue details")?;
tech_review_queue::add_projects_with_review_details(
&[DBProjectId::from(report.project_id)],
tech_review_sync::TechReviewExitReason::Resolved,
&mut transaction,
)
.await
@@ -97,26 +97,23 @@ async fn fetch_unreviewed_tech_review_project_ids(
r#"
SELECT DISTINCT m.id
FROM mods m
INNER JOIN delphi_tech_review_queue queue ON queue.project_id = m.id
WHERE
EXISTS(
SELECT 1
FROM delphi_issue_details_with_statuses didws
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
WHERE
didws.project_id = m.id
AND didws.status = 'pending'
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
AND didws.severity != 'hidden'
)
AND NOT EXISTS(
SELECT 1
FROM delphi_issue_details_with_statuses didws
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
WHERE
didws.project_id = m.id
AND didws.status IN ('safe', 'unsafe')
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
AND didws.severity != 'hidden'
)
"#,
)
@@ -0,0 +1,203 @@
//! Maintains explicit project membership in the technical review queue.
//!
//! Queue membership is represented by a row in `delphi_tech_review_queue`.
//! Enter and exit thread messages are emitted only when an insert or delete
//! actually changes that membership, in the same transaction.
use itertools::Itertools;
use crate::{
database::{
PgTransaction,
models::{DBProjectId, DBThreadId, thread_item::ThreadMessageBuilder},
},
models::threads::MessageBody,
routes::ApiError,
util::error::Context,
};
#[derive(Debug, Clone, Copy)]
pub enum TechReviewRemovalReason {
RulesChanged,
FileDeleted,
}
pub async fn add_projects(
project_ids: &[DBProjectId],
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
if project_ids.is_empty() {
return Ok(());
}
let rows = sqlx::query!(
r#"
WITH inserted AS (
INSERT INTO delphi_tech_review_queue (project_id)
SELECT unnest($1::bigint[])
ON CONFLICT (project_id) DO NOTHING
RETURNING project_id
)
SELECT
inserted.project_id AS "project_id!: DBProjectId",
(
SELECT thread.id
FROM threads thread
WHERE thread.mod_id = inserted.project_id
ORDER BY thread.id
LIMIT 1
) AS "thread_id!: DBThreadId"
FROM inserted
"#,
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err("failed to add projects to technical review queue")?;
for row in rows {
ThreadMessageBuilder {
author_id: None,
body: MessageBody::TechReviewEntered,
thread_id: row.thread_id,
hide_identity: false,
}
.insert(txn)
.await
.wrap_internal_err("failed to add entering technical review message")?;
}
Ok(())
}
pub async fn remove_projects(
project_ids: &[DBProjectId],
reason: TechReviewRemovalReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
if project_ids.is_empty() {
return Ok(());
}
let rows = sqlx::query!(
r#"
WITH removed AS (
DELETE FROM delphi_tech_review_queue
WHERE project_id = ANY($1::bigint[])
RETURNING project_id
)
SELECT
removed.project_id AS "project_id!: DBProjectId",
(
SELECT thread.id
FROM threads thread
WHERE thread.mod_id = removed.project_id
ORDER BY thread.id
LIMIT 1
) AS "thread_id!: DBThreadId"
FROM removed
"#,
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err(
"failed to remove projects from technical review queue",
)?;
let body = match reason {
TechReviewRemovalReason::RulesChanged => MessageBody::TechReviewExited,
TechReviewRemovalReason::FileDeleted => {
MessageBody::TechReviewExitFileDeleted
}
};
for row in rows {
ThreadMessageBuilder {
author_id: None,
body: body.clone(),
thread_id: row.thread_id,
hide_identity: false,
}
.insert(txn)
.await
.wrap_internal_err("failed to add exiting technical review message")?;
}
Ok(())
}
pub async fn add_projects_with_review_details(
project_ids: &[DBProjectId],
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
if project_ids.is_empty() {
return Ok(());
}
let rows = sqlx::query!(
r#"
SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId"
FROM delphi_issue_details_with_statuses detail
WHERE
detail.project_id = ANY($1::bigint[])
AND detail.status IN ('pending', 'unsafe')
AND detail.severity != 'hidden'
"#,
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err("failed to find projects requiring technical review")?;
add_projects(
&rows
.into_iter()
.map(|row| row.project_id)
.collect::<Vec<_>>(),
txn,
)
.await
}
pub async fn remove_projects_without_details(
project_ids: &[DBProjectId],
reason: TechReviewRemovalReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
if project_ids.is_empty() {
return Ok(());
}
let rows = sqlx::query!(
r#"
SELECT requested.project_id AS "project_id!: DBProjectId"
FROM unnest($1::bigint[]) AS requested(project_id)
WHERE NOT EXISTS (
SELECT 1
FROM delphi_issue_details_with_statuses detail
WHERE detail.project_id = requested.project_id
)
"#,
&project_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err(
"failed to find projects without technical review details",
)?;
remove_projects(
&rows
.into_iter()
.map(|row| row.project_id)
.collect::<Vec<_>>(),
reason,
txn,
)
.await
}
@@ -1,427 +0,0 @@
//! Synchronizes moderation thread messages and dummy queue blockers with the
//! current computed tech review state of affected projects.
//!
//! When a project has a Delphi report submitted for it, or when a moderator
//! updates one of its issue details' rows (like flagging a detail as *globally*
//! safe or unsafe), we will need to recheck if the project still belongs in the
//! tech review queue or if it needs to exit now.
//!
//! Side-note: "entering the queue" or "exiting the queue" right now just means
//! adding a new message to the project's moderation thread which indicates if
//! it entered/exited. In the future this should be replaced with a more proper
//! audit log table, or "is project currently in tech review" table.
//!
//! A project is considered to need tech review when it has at least one
//! non-dummy issue detail whose effective status is pending or unsafe, or when
//! it already has a dummy pending detail blocking the final review submission.
//! Effective status is just the local detail's verdict (from
//! `delphi_issue_detail_verdicts`), or if it's null then the global verdict for
//! the same `drid.key` (from `delphi_global_detail_verdicts`).
//!
//! Some examples of how this behavior manifests: let's assume you have projects
//! _A_ and _B_ currently in tech review. They each have one (unresolved) issue
//! detail with key _K_.
//! - If you mark _K_ on _A_ as locally safe/unsafe, then _A_ is fully resolved,
//! but we still have the `__dummy` detail, which means it's still in the
//! queue until the moderator submits the actual report. _B_ is entirely
//! unaffected.
//! - If you mark _K_ on _A_ as globally safe, then _A_ and _B_ both get fully
//! resolved, but both still have the `__dummy` detail, so they also still
//! need the final report to be submitted by the moderator.
//!
//! In practice, this means that some projects may have e.g. "100/100 traces
//! are safe" reported, but they will just be waiting for final moderator
//! approval.
//!
//! The logic for checking whether a project is now in tech review or not, and
//! sending the appropriate message, is complex! That's why this module exists:
//! to act as a single chokepoint which (correctly) syncs all the state, instead
//! of having each mutation run its own ad-hoc update logic.
use crate::util::error::ApiContext as _;
use itertools::Itertools;
use crate::{
database::{
PgTransaction,
models::{
DBProjectId, DBThreadId, DelphiReportId,
delphi_report_item::DelphiVerdict,
thread_item::ThreadMessageBuilder,
},
},
models::threads::MessageBody,
routes::ApiError,
util::error::Context,
};
const DUMMY_ISSUE_TYPE: &str = "__dummy";
#[derive(Debug, Clone, Copy)]
pub enum TechReviewExitReason {
Resolved,
FileDeleted,
}
struct ProjectTechReviewState {
has_pending_detail: bool,
has_unsafe_detail: bool,
has_dummy: bool,
thread_id: Option<DBThreadId>,
report_id: Option<DelphiReportId>,
last_tech_review_message_type: Option<String>,
}
pub async fn sync_project_tech_review_state(
project_ids: &[DBProjectId],
exit_reason: TechReviewExitReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let project_ids = project_ids.iter().copied().unique().collect::<Vec<_>>();
if project_ids.is_empty() {
return Ok(());
}
let project_ids_raw = project_ids.iter().map(|id| id.0).collect::<Vec<_>>();
let tech_review_message_types = tech_review_message_types();
let rows = sqlx::query!(
r#"
WITH project_ids AS (
SELECT unnest($1::bigint[]) AS project_id
),
detail_states AS (
SELECT
p.project_id,
COALESCE(
BOOL_OR(
didws.status = 'pending'
AND dri.issue_type != $3
),
FALSE
) AS has_pending_detail,
COALESCE(
BOOL_OR(
didws.status = 'unsafe'
AND dri.issue_type != $3
),
FALSE
) AS has_unsafe_detail,
COALESCE(
BOOL_OR(
didws.status = 'pending'
AND dri.issue_type = $3
),
FALSE
) AS has_dummy
FROM project_ids p
LEFT JOIN delphi_issue_details_with_statuses didws
ON didws.project_id = p.project_id
LEFT JOIN delphi_report_issues dri ON dri.id = didws.issue_id
GROUP BY p.project_id
)
SELECT
p.project_id AS "project_id!: DBProjectId",
p.has_pending_detail AS "has_pending_detail!",
p.has_unsafe_detail AS "has_unsafe_detail!",
p.has_dummy AS "has_dummy!",
(
SELECT t.id
FROM threads t
WHERE t.mod_id = p.project_id
ORDER BY t.id
LIMIT 1
) AS "thread_id: DBThreadId",
(
SELECT tm.body->>'type'
FROM threads t
INNER JOIN threads_messages tm ON tm.thread_id = t.id
WHERE
t.mod_id = p.project_id
AND tm.body->>'type' = ANY($2::text[])
ORDER BY tm.created DESC, tm.id DESC
LIMIT 1
) AS "last_tech_review_message_type",
(
SELECT dr.id
FROM versions v
INNER JOIN files f ON f.version_id = v.id
INNER JOIN delphi_reports dr ON dr.file_id = f.id
WHERE v.mod_id = p.project_id
ORDER BY dr.created DESC, dr.id DESC
LIMIT 1
) AS "report_id: DelphiReportId"
FROM detail_states p
"#,
&project_ids_raw,
&tech_review_message_types,
DUMMY_ISSUE_TYPE,
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err("failed to fetch project tech review state")?;
for row in rows {
let state = ProjectTechReviewState {
has_pending_detail: row.has_pending_detail,
has_unsafe_detail: row.has_unsafe_detail,
has_dummy: row.has_dummy,
thread_id: row.thread_id,
report_id: row.report_id,
last_tech_review_message_type: row.last_tech_review_message_type,
};
sync_one_project_tech_review_state(state, exit_reason, txn)
.await
.wrap_api_err("executing `sync_one_project_tech_review_state`")?;
}
Ok(())
}
pub async fn sync_detail_key_tech_review_state(
detail_keys: &[String],
exit_reason: TechReviewExitReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let detail_keys = detail_keys.iter().cloned().unique().collect::<Vec<_>>();
if detail_keys.is_empty() {
return Ok(());
}
let rows = sqlx::query!(
r#"
SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId"
FROM delphi_issue_details_with_statuses didws
WHERE didws.key = ANY($1::text[])
"#,
&detail_keys,
)
.fetch_all(&mut *txn)
.await
.wrap_internal_err("failed to fetch projects affected by detail keys")?;
let project_ids = rows
.into_iter()
.map(|row| row.project_id)
.collect::<Vec<_>>();
sync_project_tech_review_state(&project_ids, exit_reason, txn).await
}
pub async fn sync_deleted_project_tech_review_exit(
project_id: DBProjectId,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let tech_review_message_types = tech_review_message_types();
let row = sqlx::query!(
r#"
SELECT
(
SELECT t.id
FROM threads t
WHERE t.mod_id = $1
ORDER BY t.id
LIMIT 1
) AS "thread_id: DBThreadId",
(
SELECT tm.body->>'type'
FROM threads t
INNER JOIN threads_messages tm ON tm.thread_id = t.id
WHERE
t.mod_id = $1
AND tm.body->>'type' = ANY($2::text[])
ORDER BY tm.created DESC, tm.id DESC
LIMIT 1
) AS "last_tech_review_message_type"
"#,
project_id as DBProjectId,
&tech_review_message_types,
)
.fetch_one(&mut *txn)
.await
.wrap_internal_err("failed to fetch deleted project tech review state")?;
if let Some(thread_id) = row.thread_id
&& should_send_exit(row.last_tech_review_message_type.as_deref())
{
insert_exit_message(thread_id, TechReviewExitReason::FileDeleted, txn)
.await
.wrap_api_err("executing `insert_exit_message`")?;
}
Ok(())
}
async fn sync_one_project_tech_review_state(
state: ProjectTechReviewState,
exit_reason: TechReviewExitReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let needs_tech_review =
state.has_pending_detail || state.has_unsafe_detail || state.has_dummy;
if needs_tech_review {
if (state.has_pending_detail || state.has_unsafe_detail)
&& !state.has_dummy
&& let Some(report_id) = state.report_id
{
// TODO: Currently, the queue query determines whether a project is
// in tech review by checking whether it has any pending issue
// details. If all visible issue details are marked safe or unsafe
// before the final report is submitted, the project would otherwise
// leave the tech review queue without a final tech review verdict
// message.
//
// This should be replaced with explicit tech review state, such as
// an append-only project tech review event table where the latest
// enter/exit event is the current state. Until then, this dummy
// issue detail acts as the pending queue blocker.
ensure_dummy_issue_detail(report_id, txn)
.await
.wrap_api_err("validating dummy issue detail")?;
}
if let Some(thread_id) = state.thread_id
&& state.last_tech_review_message_type.as_deref()
!= Some(MessageBody::TechReviewEntered.as_ref())
{
ThreadMessageBuilder {
author_id: None,
body: MessageBody::TechReviewEntered,
thread_id,
hide_identity: false,
}
.insert(txn)
.await
.wrap_internal_err("failed to add entering tech review message")?;
}
return Ok(());
}
if matches!(exit_reason, TechReviewExitReason::Resolved)
&& state.last_tech_review_message_type.as_deref()
== Some(MessageBody::TechReviewEntered.as_ref())
{
if let Some(report_id) = state.report_id {
ensure_dummy_issue_detail(report_id, txn)
.await
.wrap_api_err("validating dummy issue detail")?;
}
return Ok(());
}
if let Some(thread_id) = state.thread_id
&& should_send_exit(state.last_tech_review_message_type.as_deref())
{
insert_exit_message(thread_id, exit_reason, txn)
.await
.wrap_api_err("executing `insert_exit_message`")?;
}
Ok(())
}
fn should_send_exit(last_tech_review_message_type: Option<&str>) -> bool {
matches!(last_tech_review_message_type, Some(message_type) if !matches!(
message_type,
message_type if message_type == MessageBody::TechReviewExited.as_ref()
|| message_type == MessageBody::TechReviewExitFileDeleted.as_ref()
|| message_type == tech_review_completed_message_type()
))
}
async fn insert_exit_message(
thread_id: DBThreadId,
exit_reason: TechReviewExitReason,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
let body = match exit_reason {
TechReviewExitReason::Resolved => MessageBody::TechReviewExited,
TechReviewExitReason::FileDeleted => {
MessageBody::TechReviewExitFileDeleted
}
};
ThreadMessageBuilder {
author_id: None,
body,
thread_id,
hide_identity: false,
}
.insert(txn)
.await
.wrap_internal_err("failed to add exiting tech review message")?;
Ok(())
}
async fn ensure_dummy_issue_detail(
report_id: DelphiReportId,
txn: &mut PgTransaction<'_>,
) -> Result<(), ApiError> {
sqlx::query!(
r#"
WITH dummy_issue AS (
INSERT INTO delphi_report_issues (report_id, issue_type)
VALUES ($1, $2)
ON CONFLICT (report_id, issue_type)
DO UPDATE SET issue_type = EXCLUDED.issue_type
RETURNING id
)
INSERT INTO delphi_report_issue_details (
issue_id,
key,
jar,
file_path,
decompiled_source,
data,
severity
)
SELECT
id,
'',
NULL,
'',
NULL,
'{}'::jsonb,
'low'::delphi_severity
FROM dummy_issue
WHERE NOT EXISTS (
SELECT 1
FROM delphi_report_issue_details drid
WHERE drid.issue_id = dummy_issue.id
)
"#,
report_id as DelphiReportId,
DUMMY_ISSUE_TYPE,
)
.execute(&mut *txn)
.await
.wrap_internal_err("failed to ensure dummy Delphi report issue detail")?;
Ok(())
}
fn tech_review_message_types() -> Vec<String> {
[
MessageBody::TechReviewEntered.as_ref(),
MessageBody::TechReviewExited.as_ref(),
MessageBody::TechReviewExitFileDeleted.as_ref(),
tech_review_completed_message_type(),
]
.into_iter()
.map(|message_type| message_type.to_string())
.collect()
}
fn tech_review_completed_message_type() -> &'static str {
MessageBody::TechReview {
verdict: DelphiVerdict::Safe,
}
.as_ref()
}
+9
View File
@@ -121,6 +121,15 @@ pub fn config(cfg: &mut web::ServiceConfig) {
moderation::tech_review::search_projects,
moderation::tech_review::global::search_global_issue_details,
moderation::tech_review::global::get_global_issue_detail,
moderation::tech_review::rules::get_rules,
moderation::tech_review::rules::test_rule,
moderation::tech_review::rules::get_rule_affected_details,
moderation::tech_review::rules::create_rule,
moderation::tech_review::rules::update_rule,
moderation::tech_review::rules::delete_rule,
moderation::tech_review::rules_scan::get_rule_schema,
moderation::tech_review::rules_scan::get_detail_rule_input,
moderation::tech_review::rules_scan::scan_rules,
moderation::tech_review::get_project_report,
moderation::tech_review::submit_report,
moderation::tech_review::update_issue_details,
@@ -297,6 +297,7 @@ pub async fn get_projects_internal(
FROM delphi_issue_details_with_statuses didws
WHERE didws.project_id = m.id
AND didws.status = 'pending'
AND didws.severity != 'hidden'
)
)
),
@@ -546,6 +547,7 @@ pub async fn get_projects_internal(
FROM delphi_issue_details_with_statuses didws
WHERE didws.project_id = m.id
AND didws.status = 'pending'
AND didws.severity != 'hidden'
)
)
),
@@ -777,6 +779,7 @@ pub async fn get_project_ids(
FROM delphi_issue_details_with_statuses didws
WHERE didws.project_id = m.id
AND didws.status = 'pending'
AND didws.severity != 'hidden'
)
)
),
@@ -905,6 +908,7 @@ pub async fn get_project_ids(
FROM delphi_issue_details_with_statuses didws
WHERE didws.project_id = mods.id
AND didws.status = 'pending'
AND didws.severity != 'hidden'
)
)
ORDER BY
@@ -34,10 +34,7 @@ use crate::{
queue::session::AuthQueue,
routes::{
ApiError,
internal::{
delphi::tech_review_sync::{self, TechReviewExitReason},
moderation::Ownership,
},
internal::{delphi::tech_review_queue, moderation::Ownership},
},
search::SearchState,
util::error::Context,
@@ -45,10 +42,14 @@ use crate::{
use eyre::eyre;
pub mod global;
pub mod rules;
pub mod rules_scan;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(search_projects)
.configure(global::config)
.configure(rules::config)
.configure(rules_scan::config)
.service(get_project_report)
.service(get_report)
.service(get_issue)
@@ -252,7 +253,9 @@ pub async fn get_issue(
)
), '[]'::jsonb)
FROM delphi_issue_details_with_statuses didws
WHERE didws.issue_id = dri.id
WHERE
didws.issue_id = dri.id
AND didws.severity != 'hidden'
)
) AS "data!: sqlx::types::Json<FileIssue>"
FROM delphi_report_issues dri
@@ -308,6 +311,15 @@ pub async fn get_report(
'file_size', f.size,
'flag_reason', 'delphi',
'download_url', f.url,
'severity', COALESCE((
SELECT MAX(didws.severity)
FROM delphi_report_issues severity_issue
INNER JOIN delphi_issue_details_with_statuses didws
ON didws.issue_id = severity_issue.id
WHERE
severity_issue.report_id = dr.id
AND didws.severity != 'hidden'
), 'low'::delphi_severity),
-- TODO: replace with `json_array` in Postgres 16
'issues', (
SELECT coalesce(json_agg(
@@ -330,15 +342,22 @@ pub async fn get_report(
)
), '[]'::jsonb)
FROM delphi_issue_details_with_statuses didws
WHERE didws.issue_id = dri.id
WHERE
didws.issue_id = dri.id
AND didws.severity != 'hidden'
)
)
), '[]'::json)
FROM delphi_report_issues dri
WHERE
dri.report_id = dr.id
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
dri.report_id = dr.id
AND EXISTS (
SELECT 1
FROM delphi_issue_details_with_statuses visible_detail
WHERE
visible_detail.issue_id = dri.id
AND visible_detail.severity != 'hidden'
)
)
) AS "data!: sqlx::types::Json<FileReport>"
FROM delphi_reports dr
@@ -533,7 +552,9 @@ async fn fetch_project_reports(
didws.global_status AS "global_status?: DelphiStatus",
didws.status AS "status!: DelphiStatus"
FROM delphi_issue_details_with_statuses didws
WHERE didws.issue_id = ANY($1::bigint[])
WHERE
didws.issue_id = ANY($1::bigint[])
AND didws.severity != 'hidden'
ORDER BY didws.issue_id, didws.id
"#,
&issue_ids.iter().map(|i| i.0).collect::<Vec<_>>()
@@ -631,14 +652,14 @@ async fn fetch_project_reports(
let mut file_issues = Vec::new();
for issue_row in report_issues {
if issue_row.issue_type == "__dummy" {
continue;
}
let issue_details = details_by_issue
.get(&issue_row.id)
.unwrap_or(&empty_details);
if issue_details.is_empty() {
continue;
}
file_issues.push(FileIssue {
id: issue_row.id,
report_id: issue_row.report_id,
@@ -647,12 +668,23 @@ async fn fetch_project_reports(
});
}
if file_issues.is_empty() {
continue;
}
let severity = file_issues
.iter()
.flat_map(|issue| issue.details.iter())
.map(|detail| detail.severity)
.max()
.unwrap_or(report_row.severity);
file_reports.push(FileReport {
report_id: report_row.report_id,
file_id: FileId::from(file_row.file_id),
created: report_row.created,
flag_reason: FlagReason::Delphi,
severity: report_row.severity,
severity,
file_name: file_row.filename.clone(),
file_size: file_row.size,
download_url: file_row.url.clone(),
@@ -732,13 +764,15 @@ pub async fn search_projects(
m.id AS "project_id: DBProjectId",
MIN(t.id) AS "thread_id!: DBThreadId"
FROM mods m
INNER JOIN delphi_tech_review_queue trq ON trq.project_id = m.id
INNER JOIN threads t ON t.mod_id = m.id
INNER JOIN versions v ON v.mod_id = m.id
INNER JOIN files f ON f.version_id = v.id
INNER JOIN delphi_reports dr ON dr.file_id = f.id
INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id
INNER JOIN delphi_issue_details_with_statuses didws
LEFT JOIN versions v ON v.mod_id = m.id
LEFT JOIN files f ON f.version_id = v.id
LEFT JOIN delphi_reports dr ON dr.file_id = f.id
LEFT JOIN delphi_report_issues dri ON dri.report_id = dr.id
LEFT JOIN delphi_issue_details_with_statuses didws
ON didws.issue_id = dri.id
AND didws.severity != 'hidden'
LEFT JOIN threads_messages tm_last
ON tm_last.thread_id = t.id
AND tm_last.id = (
@@ -782,8 +816,25 @@ pub async fn search_projects(
)
AND m.status NOT IN ('draft', 'rejected', 'withheld')
AND (cardinality($6::text[]) = 0 OR m.status = ANY($6::text[]))
AND (cardinality($7::text[]) = 0 OR dri.issue_type = ANY($7::text[]))
AND didws.status = 'pending'
AND (
cardinality($7::text[]) = 0
OR EXISTS (
SELECT 1
FROM versions issue_version
INNER JOIN files issue_file
ON issue_file.version_id = issue_version.id
INNER JOIN delphi_reports issue_report
ON issue_report.file_id = issue_file.id
INNER JOIN delphi_report_issues issue
ON issue.report_id = issue_report.id
INNER JOIN delphi_issue_details_with_statuses detail
ON detail.issue_id = issue.id
WHERE
issue_version.mod_id = m.id
AND issue.issue_type = ANY($7::text[])
AND detail.severity != 'hidden'
)
)
AND (
$5::text IS NULL
OR ($5::text = 'unreplied' AND (tm_last.id IS NULL OR u_last.role IS NULL OR u_last.role NOT IN ('moderator', 'admin')))
@@ -793,8 +844,8 @@ pub async fn search_projects(
ORDER BY
CASE WHEN $3 = 'created_asc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END ASC,
CASE WHEN $3 = 'created_desc' THEN MIN(dr.created) ELSE TO_TIMESTAMP(0) END DESC,
CASE WHEN $3 = 'severity_asc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END ASC,
CASE WHEN $3 = 'severity_desc' THEN MAX(dr.severity) ELSE 'low'::delphi_severity END DESC,
CASE WHEN $3 = 'severity_asc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END ASC,
CASE WHEN $3 = 'severity_desc' THEN COALESCE(MAX(didws.severity), 'low'::delphi_severity) ELSE 'low'::delphi_severity END DESC,
-- tie-breaker: oldest reports
MIN(dr.created) ASC
LIMIT $1 OFFSET $2
@@ -1044,8 +1095,7 @@ pub async fn submit_report(
WHERE
m.id = $1
AND didws.status = 'pending'
-- see delphi.rs todo comment
AND dri.issue_type != '__dummy'
AND didws.severity != 'hidden'
"#,
project_id as _,
)
@@ -1063,24 +1113,18 @@ pub async fn submit_report(
)));
}
sqlx::query!(
"
DELETE FROM delphi_report_issue_details drid
WHERE issue_id IN (
SELECT dri.id
FROM mods m
INNER JOIN versions v ON v.mod_id = m.id
INNER JOIN files f ON f.version_id = v.id
INNER JOIN delphi_reports dr ON dr.file_id = f.id
INNER JOIN delphi_report_issues dri ON dri.report_id = dr.id
WHERE m.id = $1 AND dri.issue_type = '__dummy'
)
",
project_id as _,
sqlx::query_scalar!(
r#"
DELETE FROM delphi_tech_review_queue
WHERE project_id = $1
RETURNING project_id AS "project_id: DBProjectId"
"#,
project_id as DBProjectId,
)
.execute(&mut txn)
.fetch_optional(&mut txn)
.await
.wrap_internal_err("failed to delete dummy issue")?;
.wrap_internal_err("failed to remove project from technical review queue")?
.wrap_not_found_err("project not found in technical review queue")?;
let record = sqlx::query!(
r#"
@@ -1269,10 +1313,6 @@ pub async fn update_issue_details(
i.verdict
FROM incoming i
INNER JOIN delphi_issue_details_with_statuses didws ON didws.id = i.detail_id
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
WHERE
-- see delphi.rs todo comment
dri.issue_type != '__dummy'
),
validated AS (
SELECT
@@ -1336,10 +1376,7 @@ pub async fn update_issue_details(
r#"
SELECT DISTINCT didws.project_id AS "project_id!: DBProjectId"
FROM delphi_issue_details_with_statuses didws
INNER JOIN delphi_report_issues dri ON dri.id = didws.issue_id
WHERE
didws.id = ANY($1::bigint[])
AND dri.issue_type != '__dummy'
WHERE didws.id = ANY($1::bigint[])
"#,
&detail_ids,
)
@@ -1354,9 +1391,8 @@ pub async fn update_issue_details(
.map(|row| row.project_id)
.collect::<Vec<_>>();
tech_review_sync::sync_project_tech_review_state(
tech_review_queue::add_projects_with_review_details(
&affected_project_ids,
TechReviewExitReason::Resolved,
&mut txn,
)
.await
@@ -1381,7 +1417,7 @@ pub async fn update_issue_details(
security(("bearer_auth" = [])),
responses((status = NO_CONTENT))
)]
#[post("/global-issue-detail")]
#[post("/global-traces")]
pub async fn update_global_issue_details(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -1474,9 +1510,25 @@ pub async fn update_global_issue_details(
.await
.wrap_internal_err("failed to update global issue details")?;
tech_review_sync::sync_detail_key_tech_review_state(
let affected_projects = sqlx::query!(
r#"
SELECT DISTINCT detail.project_id AS "project_id!: DBProjectId"
FROM delphi_issue_details_with_statuses detail
WHERE detail.key = ANY($1::text[])
"#,
&detail_keys,
TechReviewExitReason::Resolved,
)
.fetch_all(&mut txn)
.await
.wrap_internal_err(
"failed to fetch projects affected by global detail updates",
)?;
tech_review_queue::add_projects_with_review_details(
&affected_projects
.into_iter()
.map(|row| row.project_id)
.collect::<Vec<_>>(),
&mut txn,
)
.await
@@ -133,7 +133,7 @@ pub struct GlobalIssueDetailTrace {
security(("bearer_auth" = [])),
responses((status = OK, body = SearchGlobalIssueDetailsResponse))
)]
#[post("/global-issue-detail/search")]
#[post("/global-traces/search")]
pub async fn search_global_issue_details(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -199,9 +199,9 @@ pub async fn search_global_issue_details(
FROM delphi_global_detail_verdicts dgdv
LEFT JOIN delphi_issue_details_with_statuses didws
ON didws.key = dgdv.detail_key
AND didws.severity != 'hidden'
LEFT JOIN delphi_report_issues dri
ON dri.id = didws.issue_id
AND dri.issue_type != '__dummy'
WHERE (
$1::text IS NULL
OR dgdv.detail_key ILIKE '%' || $1 || '%'
@@ -269,6 +269,7 @@ pub async fn search_global_issue_details(
AND didv.detail_key = didws.key
WHERE
didws.key = ANY($1::text[])
AND didws.severity != 'hidden'
AND dri.issue_type != '__dummy'
AND (
$3::text IS NULL
@@ -356,7 +357,7 @@ pub async fn search_global_issue_details(
security(("bearer_auth" = [])),
responses((status = OK, body = GetGlobalIssueDetailResponse))
)]
#[post("/global-issue-detail/local-traces")]
#[post("/global-traces/local-traces")]
pub async fn get_global_issue_detail(
req: HttpRequest,
pool: web::Data<PgPool>,
@@ -395,9 +396,9 @@ pub async fn get_global_issue_detail(
FROM delphi_global_detail_verdicts dgdv
LEFT JOIN delphi_issue_details_with_statuses didws
ON didws.key = dgdv.detail_key
AND didws.severity != 'hidden'
LEFT JOIN delphi_report_issues dri
ON dri.id = didws.issue_id
AND dri.issue_type != '__dummy'
WHERE dgdv.detail_key = $1
GROUP BY dgdv.detail_key, dgdv.verdict
"#,
@@ -440,7 +441,7 @@ pub async fn get_global_issue_detail(
WHERE
didws.key = $1
AND ($2::bigint IS NULL OR didws.id > $2)
AND dri.issue_type != '__dummy'
AND didws.severity != 'hidden'
ORDER BY didws.id
LIMIT $3
"#,
@@ -0,0 +1,664 @@
use actix_web::{HttpRequest, delete, get, post, put, web};
use chrono::{DateTime, Utc};
use eyre::eyre;
use serde::{Deserialize, Serialize};
use validator::Validate;
use xredis::RedisPool;
use super::rules_scan::RuleInput;
use crate::{
auth::check_is_moderator_from_headers,
database::{
PgPool, ReadOnlyPgPool,
models::{
DBProjectId, DBVersionId, DelphiReportIssueDetailsId,
DelphiReportIssueId, DelphiRuleId,
delphi_report_item::DelphiSeverity,
},
},
models::{
ids::{ProjectId, VersionId},
pats::Scopes,
},
queue::session::AuthQueue,
routes::ApiError,
util::{cel, error::Context, validate::validation_errors_to_string},
};
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_rules)
.service(test_rule)
.service(get_rule_affected_details)
.service(create_rule)
.service(update_rule)
.service(delete_rule);
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct DelphiRule {
pub id: DelphiRuleId,
pub name: String,
pub rule: String,
pub priority: i32,
pub revision: i64,
pub current_revision: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: Option<i64>,
pub updated_by: Option<i64>,
pub affected_details_count: i64,
pub affected_details: Vec<DelphiRuleAffectedDetail>,
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct DelphiRuleAffectedDetail {
pub detail_id: DelphiReportIssueDetailsId,
pub issue_id: DelphiReportIssueId,
pub project_id: Option<ProjectId>,
pub project_name: Option<String>,
pub project_icon_url: Option<String>,
pub version_id: Option<VersionId>,
pub version_name: Option<String>,
pub version_number: Option<String>,
pub issue_type: String,
pub key: String,
pub jar: Option<String>,
pub file_path: String,
pub original_severity: DelphiSeverity,
pub severity: DelphiSeverity,
}
#[derive(Debug, Deserialize, Validate, utoipa::ToSchema)]
pub struct WriteDelphiRule {
#[validate(length(min = 1, max = 256))]
pub name: String,
#[validate(length(min = 1, max = 65536))]
pub rule: String,
#[serde(default)]
pub priority: i32,
}
#[derive(Deserialize, Validate, utoipa::ToSchema)]
pub struct TestDelphiRule {
#[validate(length(min = 1, max = 65536))]
pub rule: String,
#[validate(length(max = 10))]
pub inputs: Vec<RuleInput>,
}
#[derive(Debug, Serialize, utoipa::ToSchema)]
pub struct TestDelphiRuleResponse {
pub effects: Vec<Option<DelphiRuleEffect>>,
}
#[derive(Debug, Deserialize, Serialize, utoipa::ToSchema)]
#[serde(deny_unknown_fields)]
pub struct DelphiRuleEffect {
pub severity: DelphiSeverity,
}
#[derive(Serialize, utoipa::ToSchema)]
#[serde(untagged)]
pub enum DelphiRuleOutput {
Severity(DelphiSeverity),
Effect(DelphiRuleEffect),
}
struct ValidatedRule {
name: String,
rule: String,
priority: i32,
}
impl WriteDelphiRule {
async fn validate(mut self) -> Result<ValidatedRule, ApiError> {
self.name = self.name.trim().to_string();
self.rule = self.rule.trim().to_string();
Validate::validate(&self).map_err(|error| {
ApiError::Request(eyre!(validation_errors_to_string(error, None)))
})?;
let expression = self.rule.clone();
tokio::task::spawn_blocking(move || cel::Program::compile(&expression))
.await
.wrap_internal_err("failed to join cel compilation task")?
.map_err(|error| {
ApiError::Request(
eyre!(error).wrap_err("invalid cel expression"),
)
})?;
Ok(ValidatedRule {
name: self.name,
rule: self.rule,
priority: self.priority,
})
}
}
/// Evaluate a CEL rule against caller-provided issue traces without saving it.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
request_body = TestDelphiRule,
responses((status = OK, body = TestDelphiRuleResponse))
)]
#[post("/rules/test")]
pub async fn test_rule(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
body: web::Json<TestDelphiRule>,
) -> Result<web::Json<TestDelphiRuleResponse>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.wrap_auth_err("authenticating API request")?;
let mut request = body.into_inner();
request.rule = request.rule.trim().to_string();
request.validate().map_err(|error| {
ApiError::Request(eyre!(validation_errors_to_string(error, None)))
})?;
let rule = request.rule;
let expression = rule.clone();
let program =
tokio::task::spawn_blocking(move || cel::Program::compile(&expression))
.await
.wrap_internal_err("failed to join cel compilation task")?
.map_err(|error| {
ApiError::Request(
eyre!(error).wrap_err("invalid cel expression"),
)
})?;
let mut effects = Vec::with_capacity(request.inputs.len());
for (index, input) in request.inputs.iter().enumerate() {
let evaluation =
super::rules_scan::evaluate_rule(&program, &rule, input);
let effect = evaluation.wrap_request_err(format!(
"failed to evaluate test input {index}"
))?;
effects.push(effect);
}
Ok(web::Json(TestDelphiRuleResponse { effects }))
}
/// List all Delphi rules that are not pending deletion.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = Vec<DelphiRule>))
)]
#[get("/rules")]
pub async fn get_rules(
req: HttpRequest,
pool: web::Data<PgPool>,
ro_pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<Vec<DelphiRule>>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.wrap_auth_err("authenticating API request")?;
let rules = sqlx::query!(
r#"
SELECT
delphi_rule.id AS "id!: DelphiRuleId",
delphi_rule.name,
delphi_rule.rule,
delphi_rule.priority,
delphi_rule.revision,
(
SELECT revision FROM delphi_rule_revisions LIMIT 1
) AS "current_revision!",
delphi_rule.created_at,
delphi_rule.updated_at,
delphi_rule.created_by,
delphi_rule.updated_by,
COALESCE(preview.affected_details_count, 0)
AS "affected_details_count!",
preview.detail_id AS "detail_id?: DelphiReportIssueDetailsId",
preview.issue_id AS "issue_id?: DelphiReportIssueId",
preview.project_id AS "project_id?: DBProjectId",
preview.project_name AS "project_name?",
preview.project_icon_url AS "project_icon_url?",
preview.version_id AS "version_id?: DBVersionId",
preview.version_name AS "version_name?",
preview.version_number AS "version_number?",
preview.issue_type AS "issue_type?",
preview.key AS "key?",
preview.jar AS "jar?",
preview.file_path AS "file_path?",
preview.original_severity AS "original_severity?: DelphiSeverity",
preview.severity AS "effect_severity?: DelphiSeverity"
FROM delphi_rules delphi_rule
LEFT JOIN LATERAL (
SELECT
effect.detail_id,
detail.issue_id,
version.mod_id AS project_id,
project.name AS project_name,
project.icon_url AS project_icon_url,
version.id AS version_id,
version.name AS version_name,
version.version_number,
issue.issue_type,
detail.key,
detail.jar,
detail.file_path,
detail.severity AS original_severity,
effect.severity,
COUNT(*) OVER () AS affected_details_count
FROM delphi_rule_effects effect
INNER JOIN delphi_rule_revisions published
ON published.revision = effect.revision
INNER JOIN delphi_report_issue_details detail
ON detail.id = effect.detail_id
INNER JOIN delphi_report_issues issue
ON issue.id = detail.issue_id
INNER JOIN delphi_reports report
ON report.id = issue.report_id
LEFT JOIN files file ON file.id = report.file_id
LEFT JOIN versions version ON version.id = file.version_id
LEFT JOIN mods project ON project.id = version.mod_id
WHERE effect.rule_id = delphi_rule.id
ORDER BY effect.detail_id DESC
LIMIT 3
) preview ON TRUE
WHERE NOT delphi_rule.delete_on_next_revision
ORDER BY
delphi_rule.priority DESC,
delphi_rule.id,
preview.detail_id DESC
"#,
)
.fetch_all(&***ro_pool)
.await
.wrap_internal_err("failed to fetch delphi rules")?;
let mut response = Vec::<DelphiRule>::new();
for rule in rules {
let affected_detail = if let (
Some(detail_id),
Some(issue_id),
Some(issue_type),
Some(key),
Some(file_path),
Some(original_severity),
Some(severity),
) = (
rule.detail_id,
rule.issue_id,
rule.issue_type,
rule.key,
rule.file_path,
rule.original_severity,
rule.effect_severity,
) {
Some(DelphiRuleAffectedDetail {
detail_id,
issue_id,
project_id: rule.project_id.map(ProjectId::from),
project_name: rule.project_name,
project_icon_url: rule.project_icon_url,
version_id: rule.version_id.map(VersionId::from),
version_name: rule.version_name,
version_number: rule.version_number,
issue_type,
key,
jar: rule.jar,
file_path,
original_severity,
severity,
})
} else {
None
};
if let Some(existing) = response.last_mut()
&& existing.id == rule.id
{
if let Some(affected_detail) = affected_detail {
existing.affected_details.push(affected_detail);
}
continue;
}
response.push(DelphiRule {
id: rule.id,
name: rule.name,
rule: rule.rule,
priority: rule.priority,
revision: rule.revision,
current_revision: rule.current_revision,
created_at: rule.created_at,
updated_at: rule.updated_at,
created_by: rule.created_by,
updated_by: rule.updated_by,
affected_details_count: rule.affected_details_count,
affected_details: affected_detail.into_iter().collect(),
});
}
Ok(web::Json(response))
}
/// List all details affected by a Delphi rule in the published revision.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK, body = Vec<DelphiRuleAffectedDetail>))
)]
#[get("/rules/{id}/effects")]
pub async fn get_rule_affected_details(
req: HttpRequest,
pool: web::Data<PgPool>,
ro_pool: web::Data<ReadOnlyPgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(DelphiRuleId,)>,
) -> Result<web::Json<Vec<DelphiRuleAffectedDetail>>, ApiError> {
check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.wrap_auth_err("authenticating API request")?;
let (rule_id,) = path.into_inner();
let details = sqlx::query!(
r#"
SELECT
effect.detail_id AS "detail_id!: DelphiReportIssueDetailsId",
detail.issue_id AS "issue_id!: DelphiReportIssueId",
version.mod_id AS "project_id?: DBProjectId",
project.name AS "project_name?",
project.icon_url AS "project_icon_url?",
version.id AS "version_id?: DBVersionId",
version.name AS "version_name?",
version.version_number AS "version_number?",
issue.issue_type,
detail.key,
detail.jar,
detail.file_path,
detail.severity AS "original_severity!: DelphiSeverity",
effect.severity AS "effect_severity!: DelphiSeverity"
FROM delphi_rule_effects effect
INNER JOIN delphi_rule_revisions published
ON published.revision = effect.revision
INNER JOIN delphi_report_issue_details detail
ON detail.id = effect.detail_id
INNER JOIN delphi_report_issues issue ON issue.id = detail.issue_id
INNER JOIN delphi_reports report ON report.id = issue.report_id
LEFT JOIN files file ON file.id = report.file_id
LEFT JOIN versions version ON version.id = file.version_id
LEFT JOIN mods project ON project.id = version.mod_id
WHERE effect.rule_id = $1
ORDER BY effect.detail_id DESC
"#,
rule_id as DelphiRuleId,
)
.fetch_all(&***ro_pool)
.await
.wrap_internal_err("failed to fetch details affected by delphi rule")?;
Ok(web::Json(
details
.into_iter()
.map(|detail| DelphiRuleAffectedDetail {
detail_id: detail.detail_id,
issue_id: detail.issue_id,
project_id: detail.project_id.map(ProjectId::from),
project_name: detail.project_name,
project_icon_url: detail.project_icon_url,
version_id: detail.version_id.map(VersionId::from),
version_name: detail.version_name,
version_number: detail.version_number,
issue_type: detail.issue_type,
key: detail.key,
jar: detail.jar,
file_path: detail.file_path,
original_severity: detail.original_severity,
severity: detail.effect_severity,
})
.collect(),
))
}
/// Create a Delphi rule. It will be applied by the next manual rule scan.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
request_body = WriteDelphiRule,
responses((status = OK, body = DelphiRule))
)]
#[post("/rules")]
pub async fn create_rule(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
body: web::Json<WriteDelphiRule>,
) -> Result<web::Json<DelphiRule>, ApiError> {
let user = check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_WRITE,
)
.await
.wrap_auth_err("authenticating API request")?;
let rule = body.into_inner().validate().await?;
let user_id = user.id.0 as i64;
let rule = sqlx::query!(
r#"
INSERT INTO delphi_rules (
name,
rule,
priority,
revision,
created_by,
updated_by
)
VALUES (
$1,
$2,
$3,
(SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1),
$4,
$4
)
RETURNING
id AS "id!: DelphiRuleId",
name,
rule,
priority,
revision,
created_at,
updated_at,
created_by,
updated_by
"#,
rule.name,
rule.rule,
rule.priority,
user_id,
)
.fetch_one(&**pool)
.await
.wrap_internal_err("failed to create delphi rule")?;
Ok(web::Json(DelphiRule {
id: rule.id,
name: rule.name,
rule: rule.rule,
priority: rule.priority,
revision: rule.revision,
current_revision: rule.revision - 1,
created_at: rule.created_at,
updated_at: rule.updated_at,
created_by: rule.created_by,
updated_by: rule.updated_by,
affected_details_count: 0,
affected_details: Vec::new(),
}))
}
/// Update a Delphi rule. Its materialized effects remain unchanged until the next scan.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
request_body = WriteDelphiRule,
responses((status = OK, body = DelphiRule), (status = NOT_FOUND))
)]
#[put("/rules/{id}")]
pub async fn update_rule(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(DelphiRuleId,)>,
body: web::Json<WriteDelphiRule>,
) -> Result<web::Json<DelphiRule>, ApiError> {
let user = check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_WRITE,
)
.await
.wrap_auth_err("authenticating API request")?;
let (id,) = path.into_inner();
let rule = body.into_inner().validate().await?;
let user_id = user.id.0 as i64;
let rule = sqlx::query!(
r#"
UPDATE delphi_rules
SET
name = $2,
rule = $3,
priority = $4,
revision = (
SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1
),
updated_at = CURRENT_TIMESTAMP,
updated_by = $5
WHERE id = $1 AND NOT delete_on_next_revision
RETURNING
id AS "id!: DelphiRuleId",
name,
rule,
priority,
revision,
created_at,
updated_at,
created_by,
updated_by
"#,
id as DelphiRuleId,
rule.name,
rule.rule,
rule.priority,
user_id,
)
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to update delphi rule")?
.wrap_not_found_err("delphi rule not found")?;
Ok(web::Json(DelphiRule {
id: rule.id,
name: rule.name,
rule: rule.rule,
priority: rule.priority,
revision: rule.revision,
current_revision: rule.revision - 1,
created_at: rule.created_at,
updated_at: rule.updated_at,
created_by: rule.created_by,
updated_by: rule.updated_by,
affected_details_count: 0,
affected_details: Vec::new(),
}))
}
/// Mark a Delphi rule for deletion when the next rule scan is published.
#[utoipa::path(
context_path = "/moderation/tech-review",
tag = "moderation",
security(("bearer_auth" = [])),
responses((status = OK), (status = NOT_FOUND))
)]
#[delete("/rules/{id}")]
pub async fn delete_rule(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
path: web::Path<(DelphiRuleId,)>,
) -> Result<(), ApiError> {
let user = check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_WRITE,
)
.await
.wrap_auth_err("authenticating API request")?;
let (id,) = path.into_inner();
let deleted = sqlx::query!(
r#"
UPDATE delphi_rules
SET
delete_on_next_revision = TRUE,
revision = (
SELECT revision + 1 FROM delphi_rule_revisions LIMIT 1
),
updated_at = CURRENT_TIMESTAMP,
updated_by = $2
WHERE id = $1 AND NOT delete_on_next_revision
RETURNING id
"#,
id as DelphiRuleId,
user.id.0 as i64,
)
.fetch_optional(&**pool)
.await
.wrap_internal_err("failed to mark delphi rule for deletion")?;
if deleted.is_none() {
return Err(ApiError::NotFound(eyre!("delphi rule not found")));
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+41 -2
View File
@@ -235,6 +235,23 @@ impl ApiError {
}
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
let report = match self {
Self::Internal(report)
| Self::Request(report)
| Self::Auth(report)
| Self::NotFound(report)
| Self::Conflict(report)
| Self::FailedDependency(report)
| Self::PreconditionRequired(report)
| Self::PreconditionFailed(report)
| Self::RateLimit(report) => report,
};
let details = report
.chain()
.skip(1)
.map(ToString::to_string)
.collect::<Vec<_>>();
crate::models::error::ApiError {
error: match self {
Self::Internal(..) => "internal_error",
@@ -247,8 +264,8 @@ impl ApiError {
Self::PreconditionFailed(..) => "precondition_failed",
Self::RateLimit(..) => "ratelimit_error",
},
description: format!("{self:#}"),
details: None,
description: report.to_string(),
details: (!details.is_empty()).then(|| serde_json::json!(details)),
}
}
}
@@ -272,3 +289,25 @@ impl actix_web::ResponseError for ApiError {
HttpResponse::build(self.status_code()).json(self.as_api_error())
}
}
#[cfg(test)]
mod tests {
use super::ApiError;
#[test]
fn api_error_serializes_source_chain_as_details() {
let error = ApiError::Request(
eyre::eyre!("root cause")
.wrap_err("intermediate context")
.wrap_err("request failed"),
);
let response = error.as_api_error();
assert_eq!(response.description, "request failed");
assert_eq!(
response.details,
Some(serde_json::json!(["intermediate context", "root cause"])),
);
}
}
+7 -9
View File
@@ -3053,6 +3053,13 @@ pub async fn project_delete_internal(
.begin()
.await
.wrap_internal_err("failed to start transaction")?;
delphi::tech_review_queue::remove_projects(
&[project.inner.id],
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
&mut transaction,
)
.await
.wrap_internal_err("removing project from technical review queue")?;
// rejected & withheld projects are transferred to ghost so moderation data is preserved
if matches!(
@@ -3216,15 +3223,6 @@ pub async fn project_delete_internal(
return Ok(());
}
delphi::tech_review_sync::sync_deleted_project_tech_review_exit(
project.inner.id,
&mut transaction,
)
.await
.wrap_api_err(
"executing `tech_review_sync::sync_deleted_project_tech_review_exit`",
)?;
let context = ImageContext::Project {
project_id: Some(project.inner.id.into()),
};
+2 -2
View File
@@ -970,9 +970,9 @@ pub async fn delete_file(
database::models::version_item::cleanup_unused_attribution_files_and_groups(&mut transaction)
.await.wrap_internal_err("deleting version item from database")?;
delphi::tech_review_sync::sync_project_tech_review_state(
delphi::tech_review_queue::remove_projects_without_details(
&[row.project_id],
delphi::tech_review_sync::TechReviewExitReason::FileDeleted,
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
&mut transaction,
)
.await
+2 -2
View File
@@ -1317,9 +1317,9 @@ pub async fn version_delete(
.await
.wrap_internal_err("deleting version from database")?;
delphi::tech_review_sync::sync_project_tech_review_state(
delphi::tech_review_queue::remove_projects_without_details(
&[version.inner.project_id],
delphi::tech_review_sync::TechReviewExitReason::FileDeleted,
delphi::tech_review_queue::TechReviewRemovalReason::FileDeleted,
&mut transaction,
)
.await
+722
View File
@@ -0,0 +1,722 @@
use std::{
collections::BTreeMap,
ops::{Deref, DerefMut},
sync::Arc,
};
use ::cel::{
Context as CelContext, ExecutionError, Program as CelProgram, Value,
};
use chumsky::{Parser, prelude::*};
use eyre::{Result, WrapErr, eyre};
use serde::Serialize;
use thiserror::Error;
use url::{Host, Url};
const MAX_PREPROCESSED_SIZE: usize = 1_048_576;
pub struct Context<'a> {
inner: CelContext<'a>,
}
impl Default for Context<'_> {
fn default() -> Self {
let mut inner = CelContext::default();
inner.add_function("url.parse", parse_url);
inner.add_function("url.is_valid", is_valid_url);
Self { inner }
}
}
impl<'a> Deref for Context<'a> {
type Target = CelContext<'a>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Context<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[derive(Serialize)]
struct ParsedUrl<'a> {
href: &'a str,
scheme: &'a str,
has_authority: bool,
cannot_be_a_base: bool,
username: &'a str,
password: Option<&'a str>,
host: Option<&'a str>,
host_type: Option<&'static str>,
domain: Option<&'a str>,
port: Option<i64>,
port_or_known_default: Option<i64>,
path: &'a str,
path_segments: Option<Vec<&'a str>>,
query: Option<&'a str>,
query_pairs: Vec<QueryPair>,
fragment: Option<&'a str>,
origin: String,
origin_is_tuple: bool,
}
#[derive(Serialize)]
struct QueryPair {
key: String,
value: String,
}
fn parse_url(value: Arc<String>) -> std::result::Result<Value, ExecutionError> {
let parsed = Url::parse(&value)
.map_err(|error| ExecutionError::function_error("url.parse", error))?;
let origin = parsed.origin();
let host_type = parsed.host().map(|host| match host {
Host::Domain(_) => "domain",
Host::Ipv4(_) => "ipv4",
Host::Ipv6(_) => "ipv6",
});
let query_pairs = parsed
.query_pairs()
.map(|(key, value)| QueryPair {
key: key.into_owned(),
value: value.into_owned(),
})
.collect();
let value = ParsedUrl {
href: parsed.as_str(),
scheme: parsed.scheme(),
has_authority: parsed.has_authority(),
cannot_be_a_base: parsed.cannot_be_a_base(),
username: parsed.username(),
password: parsed.password(),
host: parsed.host_str(),
host_type,
domain: parsed.domain(),
port: parsed.port().map(i64::from),
port_or_known_default: parsed.port_or_known_default().map(i64::from),
path: parsed.path(),
path_segments: parsed.path_segments().map(Iterator::collect),
query: parsed.query(),
query_pairs,
fragment: parsed.fragment(),
origin: origin.ascii_serialization(),
origin_is_tuple: origin.is_tuple(),
};
::cel::to_value(value)
.map_err(|error| ExecutionError::function_error("url.parse", error))
}
fn is_valid_url(value: Arc<String>) -> bool {
Url::parse(&value).is_ok()
}
pub struct Program {
inner: CelProgram,
bindings: Vec<CompiledBinding>,
}
impl Program {
pub fn compile(source: &str) -> Result<Self> {
let preprocessed = preprocess(source)
.wrap_err("failed to preprocess cel expression")?;
let bindings = preprocessed
.bindings
.into_iter()
.map(|binding| {
let inner = CelProgram::compile(&binding.expression)
.map_err(|error| eyre!(error))
.wrap_err_with(|| {
format!(
"failed to compile `#bind {}` on line {}",
binding.name, binding.line
)
})?;
Ok(CompiledBinding {
name: binding.name,
inner,
})
})
.collect::<Result<Vec<_>>>()?;
let inner = CelProgram::compile(&preprocessed.expression)
.map_err(|error| eyre!(error))?;
Ok(Self { inner, bindings })
}
pub fn execute<'a>(
&self,
context: &Context<'a>,
) -> std::result::Result<Value, ExecutionError> {
let mut context = context.inner.new_inner_scope();
for binding in &self.bindings {
let value = binding.inner.execute(&context).map_err(|error| {
ExecutionError::function_error(
&format!("#bind {}", binding.name),
error,
)
})?;
context.add_variable_from_value(&binding.name, value);
}
self.inner.execute(&context)
}
}
struct CompiledBinding {
name: String,
inner: CelProgram,
}
#[derive(Debug)]
struct Preprocessed {
expression: String,
bindings: Vec<Binding>,
}
#[derive(Debug)]
struct Definition {
name: String,
replacement: String,
}
#[derive(Debug)]
struct Binding {
name: String,
expression: String,
line: usize,
}
#[derive(Debug)]
enum Directive {
Define(Definition),
Bind { name: String, expression: String },
}
#[derive(Debug, Error)]
enum PreprocessorError {
#[error(
"invalid preprocessor directive on line {line}, column {column}: {message}"
)]
InvalidDirective {
line: usize,
column: usize,
message: String,
},
#[error("macro `{name}` is defined more than once")]
DuplicateDefinition { name: String },
#[error("binding `{name}` is declared more than once")]
DuplicateBinding { name: String },
#[error("`{name}` cannot be both a macro and a binding")]
ConflictingDirective { name: String },
#[error("recursive macro expansion: {path}")]
RecursiveExpansion { path: String },
#[error("preprocessed expression exceeds {MAX_PREPROCESSED_SIZE} bytes")]
ExpressionTooLarge,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum LexState {
#[default]
Normal,
String {
quote: u8,
triple: bool,
raw: bool,
},
BlockComment,
}
fn directive_parser() -> impl Parser<char, Directive, Error = Simple<char>> {
let horizontal_whitespace = one_of(" \t").repeated();
let identifier = filter(|character: &char| {
character.is_ascii_alphabetic() || *character == '_'
})
.then(
filter(|character: &char| {
character.is_ascii_alphanumeric() || *character == '_'
})
.repeated(),
)
.map(|(first, rest)| {
std::iter::once(first).chain(rest).collect::<String>()
});
let expression = any().repeated().at_least(1).collect::<String>();
let kind = just("define").to(true).or(just("bind").to(false));
horizontal_whitespace
.clone()
.ignore_then(just('#'))
.then_ignore(horizontal_whitespace.clone())
.ignore_then(kind)
.then_ignore(horizontal_whitespace.clone().at_least(1))
.then(identifier)
.then_ignore(horizontal_whitespace.at_least(1))
.then(expression)
.map(|((define, name), expression)| {
if define {
Directive::Define(Definition {
name,
replacement: expression.trim().to_string(),
})
} else {
Directive::Bind {
name,
expression: expression.trim().to_string(),
}
}
})
.then_ignore(end())
}
fn preprocess(source: &str) -> Result<Preprocessed, PreprocessorError> {
let mut definitions = BTreeMap::new();
let mut bindings = Vec::new();
let mut expression = String::with_capacity(source.len());
let mut state = LexState::Normal;
for (line_index, line) in source.split_inclusive('\n').enumerate() {
let directive = state == LexState::Normal
&& line.trim_start_matches([' ', '\t']).starts_with('#');
if directive {
let directive = line.trim_end_matches(['\r', '\n']);
let directive =
directive_parser().parse(directive).map_err(|errors| {
let error =
errors.into_iter().next().unwrap_or_else(|| {
Simple::custom(0..0, "invalid directive")
});
PreprocessorError::InvalidDirective {
line: line_index + 1,
column: error.span().start + 1,
message: error.to_string(),
}
})?;
match directive {
Directive::Define(definition) => {
if bindings.iter().any(|binding: &Binding| {
binding.name == definition.name
}) {
return Err(PreprocessorError::ConflictingDirective {
name: definition.name,
});
}
if definitions
.insert(definition.name.clone(), definition.replacement)
.is_some()
{
return Err(PreprocessorError::DuplicateDefinition {
name: definition.name,
});
}
}
Directive::Bind { name, expression } => {
if definitions.contains_key(&name) {
return Err(PreprocessorError::ConflictingDirective {
name,
});
}
if bindings
.iter()
.any(|binding: &Binding| binding.name == name)
{
return Err(PreprocessorError::DuplicateBinding {
name,
});
}
bindings.push(Binding {
name,
expression,
line: line_index + 1,
});
}
}
if line.ends_with('\n') {
expression.push('\n');
}
} else {
expression.push_str(line);
advance_lex_state(line, &mut state);
}
}
let mut expanded = String::with_capacity(expression.len());
expand(&expression, &definitions, &mut Vec::new(), &mut expanded)?;
for binding in &mut bindings {
let mut expanded = String::with_capacity(binding.expression.len());
expand(
&binding.expression,
&definitions,
&mut Vec::new(),
&mut expanded,
)?;
binding.expression = expanded;
}
Ok(Preprocessed {
expression: expanded,
bindings,
})
}
fn expand(
source: &str,
definitions: &BTreeMap<String, String>,
stack: &mut Vec<String>,
output: &mut String,
) -> Result<(), PreprocessorError> {
let bytes = source.as_bytes();
let mut index = 0;
let mut state = LexState::Normal;
while index < bytes.len() {
match state {
LexState::Normal => {
if bytes[index..].starts_with(b"//") {
push(output, "//")?;
index += 2;
while index < bytes.len() {
let character = source[index..]
.chars()
.next()
.expect("the index is within the string");
let mut encoded = [0; 4];
push(output, character.encode_utf8(&mut encoded))?;
index += character.len_utf8();
if character == '\n' {
break;
}
}
continue;
}
if bytes[index..].starts_with(b"/*") {
push(output, "/*")?;
index += 2;
state = LexState::BlockComment;
continue;
}
if let Some((length, string_state)) = string_start(bytes, index)
{
push(output, &source[index..index + length])?;
index += length;
state = string_state;
continue;
}
if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
let start = index;
index += 1;
while index < bytes.len()
&& (bytes[index].is_ascii_alphanumeric()
|| bytes[index] == b'_')
{
index += 1;
}
let identifier = &source[start..index];
if let Some(replacement) = definitions.get(identifier) {
if let Some(cycle_start) =
stack.iter().position(|name| name == identifier)
{
let mut path = stack[cycle_start..].to_vec();
path.push(identifier.to_string());
return Err(
PreprocessorError::RecursiveExpansion {
path: path.join(" -> "),
},
);
}
stack.push(identifier.to_string());
expand(replacement, definitions, stack, output)?;
stack.pop();
} else {
push(output, identifier)?;
}
continue;
}
}
LexState::String { quote, triple, raw } => {
if !raw && bytes[index] == b'\\' {
let end = (index + 2).min(bytes.len());
push(output, &source[index..end])?;
index = end;
continue;
}
let closing_length = if triple { 3 } else { 1 };
if bytes[index] == quote
&& bytes[index..].len() >= closing_length
&& bytes[index..index + closing_length]
.iter()
.all(|character| *character == quote)
{
push(output, &source[index..index + closing_length])?;
index += closing_length;
state = LexState::Normal;
continue;
}
}
LexState::BlockComment => {
if bytes[index..].starts_with(b"*/") {
push(output, "*/")?;
index += 2;
state = LexState::Normal;
continue;
}
}
}
let character = source[index..]
.chars()
.next()
.expect("the index is within the string");
let mut encoded = [0; 4];
push(output, character.encode_utf8(&mut encoded))?;
index += character.len_utf8();
}
Ok(())
}
fn push(output: &mut String, value: &str) -> Result<(), PreprocessorError> {
if output.len().saturating_add(value.len()) > MAX_PREPROCESSED_SIZE {
return Err(PreprocessorError::ExpressionTooLarge);
}
output.push_str(value);
Ok(())
}
fn advance_lex_state(source: &str, state: &mut LexState) {
let bytes = source.as_bytes();
let mut index = 0;
while index < bytes.len() {
match *state {
LexState::Normal => {
if bytes[index..].starts_with(b"//") {
return;
}
if bytes[index..].starts_with(b"/*") {
*state = LexState::BlockComment;
index += 2;
continue;
}
if let Some((length, string_state)) = string_start(bytes, index)
{
*state = string_state;
index += length;
continue;
}
}
LexState::String { quote, triple, raw } => {
if !raw && bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
let closing_length = if triple { 3 } else { 1 };
if bytes[index] == quote
&& bytes[index..].len() >= closing_length
&& bytes[index..index + closing_length]
.iter()
.all(|character| *character == quote)
{
*state = LexState::Normal;
index += closing_length;
continue;
}
}
LexState::BlockComment => {
if bytes[index..].starts_with(b"*/") {
*state = LexState::Normal;
index += 2;
continue;
}
}
}
index += source[index..]
.chars()
.next()
.expect("the index is within the string")
.len_utf8();
}
if matches!(state, LexState::String { triple: false, .. }) {
*state = LexState::Normal;
}
}
fn string_start(bytes: &[u8], index: usize) -> Option<(usize, LexState)> {
let remaining = &bytes[index..];
let (prefix_length, raw) = if remaining.len() >= 3
&& matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r')
&& matches!(remaining[1].to_ascii_lowercase(), b'b' | b'r')
&& remaining[0].to_ascii_lowercase()
!= remaining[1].to_ascii_lowercase()
{
(2, true)
} else if remaining.len() >= 2
&& matches!(remaining[0].to_ascii_lowercase(), b'b' | b'r')
{
(1, remaining[0].eq_ignore_ascii_case(&b'r'))
} else {
(0, false)
};
let quote = *remaining.get(prefix_length)?;
if !matches!(quote, b'\'' | b'"') {
return None;
}
let triple =
remaining
.get(prefix_length..prefix_length + 3)
.is_some_and(|quotes| {
quotes.iter().all(|character| *character == quote)
});
let quote_length = if triple { 3 } else { 1 };
Some((
prefix_length + quote_length,
LexState::String { quote, triple, raw },
))
}
#[cfg(test)]
mod tests {
use super::{Context, Program, Value, preprocess};
#[test]
fn expands_object_macros() {
let source =
"#define ISSUE OBFUSCATED_NAMES\ntrace.issue_type == ISSUE";
assert_eq!(
preprocess(source).unwrap().expression,
"\ntrace.issue_type == OBFUSCATED_NAMES"
);
}
#[test]
fn recursively_expands_macros() {
let source =
"#define RESULT SEVERITY\n#define SEVERITY \"low\"\nRESULT";
assert_eq!(preprocess(source).unwrap().expression, "\n\n\"low\"");
}
#[test]
fn does_not_expand_strings_or_comments() {
let source =
"#define VALUE expanded\nVALUE == \"VALUE\" // VALUE\n/* VALUE */";
assert_eq!(
preprocess(source).unwrap().expression,
"\nexpanded == \"VALUE\" // VALUE\n/* VALUE */"
);
}
#[test]
fn reports_recursive_macros() {
let source = "#define FIRST SECOND\n#define SECOND FIRST\nFIRST";
let error = preprocess(source).unwrap_err();
assert_eq!(
error.to_string(),
"recursive macro expansion: FIRST -> SECOND -> FIRST"
);
}
#[test]
fn evaluates_runtime_bindings_in_order() {
let source = "#bind DOUBLE input * 2\n#bind RESULT DOUBLE + 1\nRESULT";
let program = Program::compile(source).unwrap();
let mut context = Context::default();
context.add_variable("input", 4).unwrap();
assert_eq!(program.execute(&context).unwrap(), Value::Int(9));
}
#[test]
fn expands_definitions_in_runtime_bindings() {
let source =
"#define MULTIPLIER 3\n#bind RESULT input * MULTIPLIER\nRESULT";
let program = Program::compile(source).unwrap();
let mut context = Context::default();
context.add_variable("input", 4).unwrap();
assert_eq!(program.execute(&context).unwrap(), Value::Int(12));
}
#[test]
fn bindings_shadow_context_variables_after_initialization() {
let source = "#bind VALUE VALUE + 1\nVALUE";
let program = Program::compile(source).unwrap();
let mut context = Context::default();
context.add_variable("VALUE", 4).unwrap();
assert_eq!(program.execute(&context).unwrap(), Value::Int(5));
}
#[test]
fn reports_runtime_binding_errors_with_the_binding_name() {
let program =
Program::compile("#bind RESULT missing + 1\nRESULT").unwrap();
let error = program.execute(&Context::default()).unwrap_err();
assert!(error.to_string().contains("#bind RESULT"));
}
#[test]
fn parses_urls_with_namespaced_functions() {
let source = r#"
#bind URL url.parse("https://user:password@example.com:8443/a/b?first=one&first=two#fragment")
URL.scheme == "https" &&
URL.host == "example.com" &&
URL.host_type == "domain" &&
URL.port == 8443 &&
URL.path == "/a/b" &&
URL.path_segments == ["a", "b"] &&
URL.query_pairs[1].key == "first" &&
URL.query_pairs[1].value == "two" &&
URL.fragment == "fragment"
"#;
let program = Program::compile(source).unwrap();
assert_eq!(
program.execute(&Context::default()).unwrap(),
Value::Bool(true)
);
}
#[test]
fn checks_url_validity() {
let program = Program::compile(
r#"url.is_valid("https://modrinth.com/") && !url.is_valid("not a URL")"#,
)
.unwrap();
assert_eq!(
program.execute(&Context::default()).unwrap(),
Value::Bool(true)
);
}
#[test]
fn reports_url_parse_errors() {
let program = Program::compile(r#"url.parse("not a URL")"#).unwrap();
let error = program.execute(&Context::default()).unwrap_err();
assert!(error.to_string().contains("url.parse"));
}
}
+1
View File
@@ -4,6 +4,7 @@ pub mod archon;
pub mod avalara1099;
pub mod bitflag;
pub mod captcha;
pub mod cel;
pub mod cors;
pub mod date;
pub mod error;
@@ -6,6 +6,114 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
return 'labrinth_tech_review_internal'
}
public async getRules(): Promise<Labrinth.TechReview.Internal.DelphiRule[]> {
return this.client.request<Labrinth.TechReview.Internal.DelphiRule[]>(
'/moderation/tech-review/rules',
{
api: 'labrinth',
version: 'internal',
method: 'GET',
},
)
}
public async getRuleSchema(): Promise<Labrinth.TechReview.Internal.DelphiRuleSchemaResponse> {
return this.client.request<Labrinth.TechReview.Internal.DelphiRuleSchemaResponse>(
'/moderation/tech-review/rules/schema',
{
api: 'labrinth',
version: 'internal',
method: 'GET',
},
)
}
public async getDetailRuleInput(
detailId: string,
): Promise<Labrinth.TechReview.Internal.RuleInput> {
return this.client.request<Labrinth.TechReview.Internal.RuleInput>(
`/moderation/tech-review/rules/details/${detailId}/input`,
{
api: 'labrinth',
version: 'internal',
method: 'GET',
},
)
}
public async getRuleAffectedDetails(
id: number,
): Promise<Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]> {
return this.client.request<Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]>(
`/moderation/tech-review/rules/${id}/effects`,
{
api: 'labrinth',
version: 'internal',
method: 'GET',
},
)
}
public async testRule(
request: Labrinth.TechReview.Internal.TestDelphiRuleRequest,
): Promise<Labrinth.TechReview.Internal.TestDelphiRuleResponse> {
return this.client.request<Labrinth.TechReview.Internal.TestDelphiRuleResponse>(
'/moderation/tech-review/rules/test',
{
api: 'labrinth',
version: 'internal',
method: 'POST',
body: request,
},
)
}
public async createRule(
rule: Labrinth.TechReview.Internal.WriteDelphiRule,
): Promise<Labrinth.TechReview.Internal.DelphiRule> {
return this.client.request<Labrinth.TechReview.Internal.DelphiRule>(
'/moderation/tech-review/rules',
{
api: 'labrinth',
version: 'internal',
method: 'POST',
body: rule,
},
)
}
public async updateRule(
id: number,
rule: Labrinth.TechReview.Internal.WriteDelphiRule,
): Promise<Labrinth.TechReview.Internal.DelphiRule> {
return this.client.request<Labrinth.TechReview.Internal.DelphiRule>(
`/moderation/tech-review/rules/${id}`,
{
api: 'labrinth',
version: 'internal',
method: 'PUT',
body: rule,
},
)
}
public async deleteRule(id: number): Promise<void> {
return this.client.request<void>(`/moderation/tech-review/rules/${id}`, {
api: 'labrinth',
version: 'internal',
method: 'DELETE',
})
}
public async scanRules(signal?: AbortSignal): Promise<ReadableStream<Uint8Array>> {
return this.client.stream('/moderation/tech-review/rules/scan', {
api: 'labrinth',
version: 'internal',
method: 'POST',
signal,
})
}
/**
* Search for projects awaiting technical review.
*
@@ -119,7 +227,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
public async updateGlobalIssueDetails(
data: Labrinth.TechReview.Internal.UpdateGlobalIssueRequest[],
): Promise<void> {
return this.client.request<void>('/moderation/tech-review/global-issue-detail', {
return this.client.request<void>('/moderation/tech-review/global-traces', {
api: 'labrinth',
version: 'internal',
method: 'POST',
@@ -131,7 +239,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
params: Labrinth.TechReview.Internal.SearchGlobalIssueDetailsRequest,
): Promise<Labrinth.TechReview.Internal.SearchGlobalIssueDetailsResponse> {
return this.client.request<Labrinth.TechReview.Internal.SearchGlobalIssueDetailsResponse>(
'/moderation/tech-review/global-issue-detail/search',
'/moderation/tech-review/global-traces/search',
{
api: 'labrinth',
version: 'internal',
@@ -145,7 +253,7 @@ export class LabrinthTechReviewInternalModule extends AbstractModule {
params: Labrinth.TechReview.Internal.GetGlobalIssueDetailRequest,
): Promise<Labrinth.TechReview.Internal.GetGlobalIssueDetailResponse> {
return this.client.request<Labrinth.TechReview.Internal.GetGlobalIssueDetailResponse>(
'/moderation/tech-review/global-issue-detail/local-traces',
'/moderation/tech-review/global-traces/local-traces',
{
api: 'labrinth',
version: 'internal',
@@ -2407,6 +2407,112 @@ export namespace Labrinth {
export namespace TechReview {
export namespace Internal {
export type DelphiRule = {
id: number
name: string
rule: string
priority: number
revision: number
current_revision?: number
created_at: string
updated_at: string
created_by: number | null
updated_by: number | null
affected_details_count: number
affected_details: DelphiRuleAffectedDetail[]
}
export type DelphiRuleAffectedDetail = {
detail_id: string
issue_id: string
project_id: string | null
project_name: string | null
project_icon_url: string | null
version_id: string | null
version_name: string | null
version_number: string | null
issue_type: string
key: string
jar: string | null
file_path: string
original_severity: DelphiSeverity
severity: DelphiSeverity
}
export type WriteDelphiRule = {
name: string
rule: string
priority: number
}
export type TestDelphiRuleRequest = {
rule: string
inputs: RuleInput[]
}
export type DelphiRuleEffect = {
severity: DelphiSeverity
}
export type DelphiRuleSchema = Record<string, unknown>
export type DelphiRuleSchemaResponse = {
input: DelphiRuleSchema
output: DelphiRuleSchema
components: Record<string, DelphiRuleSchema>
}
export type RuleInput = {
schema_version: number
trace: RuleTrace
sibling_traces: RuleTrace[]
scan: {
delphi_version: number
}
artifact: {
size: number | null
hashes: Record<string, string>
}
project: {
id: string | null
types: string[]
}
version: {
id: string | null
loaders: string[]
}
file: {
id: string | null
}
}
export type RuleTrace = {
key: string
issue_type: string
severity: DelphiSeverity
jar: string | null
file_path: string
data: Record<string, unknown>
}
export type TestDelphiRuleResponse = {
effects: Array<DelphiRuleEffect | null>
}
export type DelphiRuleScanPhase = 'scanning' | 'publishing' | 'complete'
export type DelphiRuleScanEvent = {
phase: DelphiRuleScanPhase
revision: number
scanned: number
total: number
effects: number
}
export type DelphiRuleScanErrorEvent = {
message: string
}
export type SearchProjectsRequest = {
limit?: number
page?: number
@@ -2652,7 +2758,7 @@ export namespace Labrinth {
export type FlagReason = 'delphi'
export type DelphiSeverity = 'low' | 'medium' | 'high' | 'severe'
export type DelphiSeverity = 'hidden' | 'low' | 'medium' | 'high' | 'severe' | 'malware'
export type DelphiReportIssueStatus = 'pending' | 'safe' | 'unsafe'
+5
View File
@@ -38,6 +38,11 @@ export type ModrinthErrorResponse = {
*/
description: string
/**
* Structured details about the error
*/
details?: unknown
/**
* Optional context about where the error occurred
*/
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
source .env
psql $DATABASE_URL < fixtures/labrinth-seed-data-202508052143.sql