mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
38
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9f245cacd | ||
|
|
641ad9942a | ||
|
|
58f89d9940 | ||
|
|
30477f3316 | ||
|
|
a7196deb98 | ||
|
|
9f28a9ba5f | ||
|
|
b5b7e9bc42 | ||
|
|
0af562ef6f | ||
|
|
e9200c7483 | ||
|
|
8c2611805b | ||
|
|
c204e00ebc | ||
|
|
5cf9d423ad | ||
|
|
ac00e2bd5d | ||
|
|
43818e4f90 | ||
|
|
7ef2492d03 | ||
|
|
37d371e46c | ||
|
|
26e1d645b8 | ||
|
|
04d00c0ba0 | ||
|
|
7f59feeb98 | ||
|
|
4c8abfb78d | ||
|
|
5ead1499cd | ||
|
|
ddb45f6aac | ||
|
|
cc77fd1b87 | ||
|
|
b92fc10e54 | ||
|
|
849943ed4e | ||
|
|
18c95e2f61 | ||
|
|
7c7efd5efe | ||
|
|
4c5e1074fd | ||
|
|
36d3aab0de | ||
|
|
88c0915066 | ||
|
|
f0dff8ebd5 | ||
|
|
8d82c228cf | ||
|
|
4b07ed720e | ||
|
|
ea42c644f3 | ||
|
|
28154689f4 | ||
|
|
3c77cc1502 | ||
|
|
b6b19e543c | ||
|
|
023c789a31 |
Generated
+80
-3
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
Generated
+192
@@ -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"
|
||||
}
|
||||
Generated
+28
@@ -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"
|
||||
}
|
||||
Generated
-29
@@ -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"
|
||||
}
|
||||
Generated
+20
@@ -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"
|
||||
}
|
||||
Generated
+28
@@ -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"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+5
-3
@@ -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"
|
||||
}
|
||||
Generated
+14
@@ -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"
|
||||
}
|
||||
Generated
+32
@@ -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"
|
||||
}
|
||||
Generated
-34
@@ -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"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM delphi_rules WHERE delete_on_next_revision",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "20a88febcaf673264ff7f00a11848f97f334779cd871caa93a6de25c6c871e61"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
+5
-3
@@ -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"
|
||||
}
|
||||
+5
-3
@@ -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"
|
||||
}
|
||||
Generated
+20
@@ -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"
|
||||
}
|
||||
Generated
+120
@@ -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"
|
||||
}
|
||||
Generated
-22
@@ -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"
|
||||
}
|
||||
Generated
-34
@@ -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"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM delphi_rule_effects WHERE revision <> $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "50a5f3a116ebe5cf88452a9aac89f73135491f170e3d619a93144b36ef429705"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
Generated
-60
@@ -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"
|
||||
}
|
||||
Generated
+34
@@ -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"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "LOCK TABLE delphi_report_issue_details IN SHARE MODE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "62d548639ed397f3619a8c3a4503f472fceb6ee0e120b511c1ed64cb200e0e9c"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
Generated
+124
@@ -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"
|
||||
}
|
||||
+3
-3
@@ -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"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "LOCK TABLE delphi_rules IN SHARE MODE",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "820810b83ce47f32bf5b0dc20e5fc3223f66e83a2b5f6efbf10eec3a927f31b2"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
Generated
-23
@@ -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"
|
||||
}
|
||||
Generated
+38
@@ -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"
|
||||
}
|
||||
Generated
+3
-1
@@ -26,10 +26,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-1
@@ -22,10 +22,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-14
@@ -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"
|
||||
}
|
||||
Generated
-20
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
Generated
-15
@@ -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"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Generated
+20
@@ -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"
|
||||
}
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE delphi_rule_revisions SET revision = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ab87c1771628979a051e4b30cd6afa32576d95e5861cc857276e099b18a5e1f4"
|
||||
}
|
||||
Generated
+20
@@ -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"
|
||||
}
|
||||
Generated
+73
@@ -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"
|
||||
}
|
||||
Generated
+23
@@ -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"
|
||||
}
|
||||
+2
-2
File diff suppressed because one or more lines are too long
Generated
-29
@@ -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"
|
||||
}
|
||||
Generated
+22
@@ -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"
|
||||
}
|
||||
Generated
-19
@@ -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"
|
||||
}
|
||||
Generated
+28
@@ -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"
|
||||
}
|
||||
Generated
+29
@@ -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"
|
||||
}
|
||||
Generated
+74
@@ -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"
|
||||
}
|
||||
Generated
-58
@@ -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"
|
||||
}
|
||||
Generated
+128
@@ -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"
|
||||
}
|
||||
Generated
-22
@@ -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"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e67fda05dacea7a0b6290e8b69932ad27e5a0dd128af9273d1d6179e60f9ea0b"
|
||||
}
|
||||
Generated
+126
@@ -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"
|
||||
}
|
||||
Generated
+3
-1
@@ -19,10 +19,12 @@
|
||||
"name": "delphi_severity",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"hidden",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"severe"
|
||||
"severe",
|
||||
"malware"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
Generated
-15
@@ -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"
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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"])),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ export type ModrinthErrorResponse = {
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Structured details about the error
|
||||
*/
|
||||
details?: unknown
|
||||
|
||||
/**
|
||||
* Optional context about where the error occurred
|
||||
*/
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
source .env
|
||||
psql $DATABASE_URL < fixtures/labrinth-seed-data-202508052143.sql
|
||||
Reference in New Issue
Block a user