allow modifying test trace in rules.vue

This commit is contained in:
aecsocket
2026-08-25 13:16:59 +01:00
parent 0af562ef6f
commit b5b7e9bc42
@@ -105,38 +105,73 @@
<section class="mt-2 flex flex-col gap-3"> <section class="mt-2 flex flex-col gap-3">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div> <div>
<h3 class="m-0 text-base font-bold text-contrast">Test traces</h3> <h3 class="m-0 text-base font-bold text-contrast">Test trace</h3>
<p class="m-0 text-sm text-secondary"> <p class="m-0 text-sm text-secondary">
These results are evaluated from the current expression. Change the trace details to evaluate the current expression.
</p> </p>
</div> </div>
<LoaderCircleIcon v-if="isTestingRule" class="size-5 animate-spin text-secondary" /> <LoaderCircleIcon v-if="isTestingRule" class="size-5 animate-spin text-secondary" />
</div> </div>
<div <div
v-for="example in previewExamples" class="grid items-stretch gap-3 md:grid-cols-[minmax(0,2fr)_auto_minmax(0,1fr)]"
:key="example.original.key"
class="grid items-stretch gap-3 md:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]"
> >
<article class="universal-card flex min-w-0 flex-col gap-3"> <article class="universal-card flex min-w-0 flex-col gap-3">
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary">Original</p> <p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary">
<div class="flex flex-wrap items-center gap-2"> Trace details
<span </p>
class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize" <div class="grid gap-3 sm:grid-cols-2">
:class="getSeverityBadgeColor(example.original.severity)" <label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
> Key
{{ example.original.severity }} <StyledInput v-model="testTraceForm.key" placeholder="unique-trace-key" />
</span> </label>
<strong class="break-all text-contrast">{{ example.original.issue_type }}</strong> <label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
Issue type
<StyledInput
v-model="testTraceForm.issueType"
placeholder="OBFUSCATED_NAMES"
/>
</label>
<label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
Severity
<select
v-model="testTraceForm.severity"
class="h-9 w-full rounded-xl border-none bg-surface-4 px-3 font-medium capitalize text-primary outline-none focus:ring-4 focus:ring-brand-shadow"
>
<option v-for="severity in TRACE_SEVERITIES" :key="severity" :value="severity">
{{ severity }}
</option>
</select>
</label>
<label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
JAR
<StyledInput
v-model="testTraceForm.jar"
placeholder="META-INF/jars/embedded.jar"
/>
</label>
</div> </div>
<dl class="m-0 grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1 text-sm"> <label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
<dt class="text-secondary">Key</dt> File path
<dd class="m-0 break-all font-mono text-contrast">{{ example.original.key }}</dd> <StyledInput
<dt class="text-secondary">File</dt> v-model="testTraceForm.filePath"
<dd class="m-0 break-all font-mono text-contrast"> placeholder="com/example/Bootstrap.class"
{{ example.original.file_path }} />
</dd> </label>
</dl> <label class="flex min-w-0 flex-col gap-1 text-sm font-semibold text-contrast">
Data (JSON)
<StyledInput
v-model="testTraceForm.data"
multiline
:rows="4"
resize="vertical"
input-class="font-mono text-sm"
:error="Boolean(traceDataError)"
/>
</label>
<p v-if="traceDataError" class="m-0 text-sm text-red">
{{ traceDataError }}
</p>
</article> </article>
<div <div
@@ -148,26 +183,40 @@
<article class="universal-card flex min-w-0 flex-col gap-3"> <article class="universal-card flex min-w-0 flex-col gap-3">
<p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary"> <p class="m-0 text-xs font-semibold uppercase tracking-wide text-secondary">
New state Rule result
</p> </p>
<div <div
v-if="example.effectiveSeverity === 'hidden'" v-if="isTestingRule"
class="flex items-center gap-2 text-secondary"
>
<LoaderCircleIcon class="size-5 animate-spin" />
<span>Evaluating</span>
</div>
<p v-else-if="traceDataError || ruleTestError" class="m-0 text-sm text-secondary">
Preview unavailable.
</p>
<div
v-else-if="testTracePreview.effectiveSeverity === 'hidden'"
class="flex items-center gap-2 text-secondary" class="flex items-center gap-2 text-secondary"
> >
<EyeOffIcon class="size-5" /> <EyeOffIcon class="size-5" />
<strong class="text-contrast">Hidden from reports</strong> <strong class="text-contrast">Hidden from reports</strong>
</div> </div>
<div v-else-if="!testTracePreview.effect" class="flex flex-col gap-2">
<strong class="text-contrast">No match</strong>
<p class="m-0 text-sm text-secondary">{{ testTracePreview.summary }}</p>
</div>
<template v-else> <template v-else>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<span <span
class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize" class="rounded-full border px-2 py-0.5 text-xs font-semibold capitalize"
:class="getSeverityBadgeColor(example.effectiveSeverity)" :class="getSeverityBadgeColor(testTracePreview.effectiveSeverity)"
> >
{{ example.effectiveSeverity }} {{ testTracePreview.effectiveSeverity }}
</span> </span>
<strong class="break-all text-contrast">{{ example.original.issue_type }}</strong> <strong class="break-all text-contrast">{{ testTraceForm.issueType }}</strong>
</div> </div>
<p class="m-0 text-sm text-secondary">{{ example.summary }}</p> <p class="m-0 text-sm text-secondary">{{ testTracePreview.summary }}</p>
</template> </template>
</article> </article>
</div> </div>
@@ -451,79 +500,67 @@ type RuleTestError = {
details: string[] details: string[]
} }
const TEST_INPUTS: Labrinth.TechReview.Internal.RuleInput[] = [ type TestTraceForm = {
{ key: string
schema_version: 1, issueType: string
trace: { severity: Labrinth.TechReview.Internal.DelphiSeverity
key: 'known-safe:obfuscated-bootstrap', jar: string
issue_type: 'OBFUSCATED_NAMES', filePath: string
severity: 'high', data: string
jar: 'META-INF/jars/embedded.jar', }
file_path: 'com/example/Bootstrap.class',
data: { const TRACE_SEVERITIES: Labrinth.TechReview.Internal.DelphiSeverity[] = [
'low',
'medium',
'high',
'severe',
'malware',
'hidden',
]
const TEST_INPUT_METADATA: Omit<Labrinth.TechReview.Internal.RuleInput, 'trace'> = {
schema_version: 1,
sibling_traces: [],
scan: {
delphi_version: 17,
},
artifact: {
size: 412_892,
hashes: {
sha1: '0123456789abcdef',
sha512: 'fedcba9876543210',
},
},
project: {
id: 'example-project',
types: ['mod'],
},
version: {
id: 'example-version',
loaders: ['fabric'],
},
file: {
id: 'example-file',
},
}
function createTestTraceForm(): TestTraceForm {
return {
key: 'known-safe:obfuscated-bootstrap',
issueType: 'OBFUSCATED_NAMES',
severity: 'high',
jar: 'META-INF/jars/embedded.jar',
filePath: 'com/example/Bootstrap.class',
data: JSON.stringify(
{
confidence: 0.97, confidence: 0.97,
symbol_count: 42, symbol_count: 42,
}, },
}, null,
sibling_traces: [], 2,
scan: { ),
delphi_version: 17, }
}, }
artifact: {
size: 412_892,
hashes: {
sha1: '0123456789abcdef',
sha512: 'fedcba9876543210',
},
},
project: {
id: 'example-project',
types: ['mod'],
},
version: {
id: 'example-version',
loaders: ['fabric'],
},
file: {
id: 'example-file',
},
},
{
schema_version: 1,
trace: {
key: 'network/known-telemetry-host',
issue_type: 'SUSPICIOUS_NETWORK_ACCESS',
severity: 'medium',
jar: null,
file_path: 'com/example/Telemetry.class',
data: {
host: 'telemetry.example.com',
},
},
sibling_traces: [],
scan: {
delphi_version: 18,
},
artifact: {
size: 98_304,
hashes: {
sha1: 'abcdef0123456789',
sha512: '0123456789abcdef',
},
},
project: {
id: 'telemetry-project',
types: ['mod'],
},
version: {
id: 'telemetry-version',
loaders: ['neoforge'],
},
file: {
id: 'telemetry-file',
},
},
]
useHead({ title: 'Delphi rules - Modrinth' }) useHead({ title: 'Delphi rules - Modrinth' })
@@ -549,6 +586,7 @@ const editingRuleId = ref<number | null>(null)
const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null) const ruleToDelete = ref<Labrinth.TechReview.Internal.DelphiRule | null>(null)
const ruleTestEffects = ref<Array<Labrinth.TechReview.Internal.DelphiRuleEffect | null>>([]) const ruleTestEffects = ref<Array<Labrinth.TechReview.Internal.DelphiRuleEffect | null>>([])
const ruleTestError = ref<RuleTestError | null>(null) const ruleTestError = ref<RuleTestError | null>(null)
const traceDataError = ref<string | null>(null)
const scanProgress = ref<Labrinth.TechReview.Internal.DelphiRuleScanEvent | null>(null) const scanProgress = ref<Labrinth.TechReview.Internal.DelphiRuleScanEvent | null>(null)
const expandedAffectedDetails = reactive( const expandedAffectedDetails = reactive(
new Map<number, Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]>(), new Map<number, Labrinth.TechReview.Internal.DelphiRuleAffectedDetail[]>(),
@@ -559,6 +597,7 @@ const form = reactive({
priority: 0 as number | undefined, priority: 0 as number | undefined,
rule: DEFAULT_RULE, rule: DEFAULT_RULE,
}) })
const testTraceForm = reactive(createTestTraceForm())
let ruleTestRequestId = 0 let ruleTestRequestId = 0
let scanAbortController: AbortController | null = null let scanAbortController: AbortController | null = null
@@ -577,32 +616,25 @@ const ruleInputSchemaText = computed(() =>
const ruleOutputSchemaText = computed(() => const ruleOutputSchemaText = computed(() =>
ruleSchema.value ? formatRuleSchema(ruleSchema.value.output, ruleSchema.value.components) : '', ruleSchema.value ? formatRuleSchema(ruleSchema.value.output, ruleSchema.value.components) : '',
) )
const previewExamples = computed(() => const testTracePreview = computed(() => {
TEST_INPUTS.map(({ trace: original }, index) => { const effect = ruleTestEffects.value[0] ?? null
const effect = ruleTestEffects.value[index] ?? null const effectiveSeverity = effect?.severity ?? testTraceForm.severity
const effectiveSeverity = effect?.severity ?? original.severity let summary: string
let summary: string
if (isTestingRule.value) { if (!effect) {
summary = 'Evaluating the current expression...' summary = 'This rule would not change the trace.'
} else if (ruleTestError.value) { } else if (effect.severity !== testTraceForm.severity) {
summary = 'Preview unavailable.' summary = `Severity changed from ${testTraceForm.severity} to ${effect.severity}.`
} else if (!effect) { } else {
summary = 'Rule does not match; no change.' summary = 'The rule matched without changing the severity.'
} else if (effect.severity && effect.severity !== original.severity) { }
summary = `Severity changed from ${original.severity} to ${effect.severity}.`
} else {
summary = 'Rule matched; no visible change.'
}
return { return {
original, effect,
effect, effectiveSeverity,
effectiveSeverity, summary,
summary, }
} })
}),
)
function isRuleLive(rule: Labrinth.TechReview.Internal.DelphiRule): boolean { function isRuleLive(rule: Labrinth.TechReview.Internal.DelphiRule): boolean {
return rule.current_revision === undefined || rule.revision === rule.current_revision return rule.current_revision === undefined || rule.revision === rule.current_revision
@@ -721,6 +753,34 @@ function handleRuleInput(rule: string) {
queueRuleTest() queueRuleTest()
} }
function getTestRuleInput(): Labrinth.TechReview.Internal.RuleInput | null {
let data: unknown
try {
data = JSON.parse(testTraceForm.data)
} catch {
traceDataError.value = 'Enter valid JSON data for the trace.'
return null
}
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
traceDataError.value = 'Trace data must be a JSON object.'
return null
}
traceDataError.value = null
return {
...TEST_INPUT_METADATA,
trace: {
key: testTraceForm.key,
issue_type: testTraceForm.issueType,
severity: testTraceForm.severity,
jar: testTraceForm.jar.trim() || null,
file_path: testTraceForm.filePath,
data: data as Record<string, unknown>,
},
}
}
async function testRule() { async function testRule() {
if (!isRuleModalOpen.value) return if (!isRuleModalOpen.value) return
@@ -728,6 +788,7 @@ async function testRule() {
const rule = form.rule.trim() const rule = form.rule.trim()
ruleTestEffects.value = [] ruleTestEffects.value = []
ruleTestError.value = null ruleTestError.value = null
traceDataError.value = null
if (!rule) { if (!rule) {
isTestingRule.value = false isTestingRule.value = false
@@ -738,11 +799,17 @@ async function testRule() {
return return
} }
const input = getTestRuleInput()
if (!input) {
isTestingRule.value = false
return
}
isTestingRule.value = true isTestingRule.value = true
try { try {
const response = await client.labrinth.tech_review_internal.testRule({ const response = await client.labrinth.tech_review_internal.testRule({
rule, rule,
inputs: TEST_INPUTS, inputs: [input],
}) })
if (requestId !== ruleTestRequestId) return if (requestId !== ruleTestRequestId) return
@@ -777,6 +844,8 @@ function queueRuleTest() {
void testRuleDebounced() void testRuleDebounced()
} }
watch(testTraceForm, queueRuleTest, { flush: 'sync' })
async function loadRules() { async function loadRules() {
isLoading.value = true isLoading.value = true
loadFailed.value = false loadFailed.value = false
@@ -855,6 +924,7 @@ function openCreateModal() {
form.name = '' form.name = ''
form.priority = 0 form.priority = 0
form.rule = DEFAULT_RULE form.rule = DEFAULT_RULE
Object.assign(testTraceForm, createTestTraceForm())
isRuleModalOpen.value = true isRuleModalOpen.value = true
ruleModal.value?.show() ruleModal.value?.show()
nextTick(() => ruleEditorInstance.value?.resize(true)) nextTick(() => ruleEditorInstance.value?.resize(true))
@@ -868,6 +938,7 @@ function openEditModal(rule: Labrinth.TechReview.Internal.DelphiRule) {
form.name = rule.name form.name = rule.name
form.priority = rule.priority form.priority = rule.priority
form.rule = rule.rule form.rule = rule.rule
Object.assign(testTraceForm, createTestTraceForm())
isRuleModalOpen.value = true isRuleModalOpen.value = true
ruleModal.value?.show() ruleModal.value?.show()
nextTick(() => ruleEditorInstance.value?.resize(true)) nextTick(() => ruleEditorInstance.value?.resize(true))