| 1 | <template> |
| 2 | <n-spin :show="loading"> |
| 3 | <div v-if="rule" class="flex flex-col gap-4 pb-1"> |
| 4 | <!-- Basic Information --> |
| 5 | <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> |
| 6 | <PropsList :list="infoFields" embedded title="Information" /> |
| 7 | <PropsList :list="riskAssessmentFields" embedded title="Risk Assessment" /> |
| 8 | </div> |
| 9 | |
| 10 | <!-- Description --> |
| 11 | <CardKV> |
| 12 | <template #key>Description</template> |
| 13 | <template #value>{{ rule.description }}</template> |
| 14 | </CardKV> |
| 15 | |
| 16 | <!-- Graylog Query --> |
| 17 | <CardKV v-if="rule.graylog?.query"> |
| 18 | <template #key> |
| 19 | <div class="flex items-center justify-between gap-2"> |
| 20 | <div class="flex items-center gap-2"> |
| 21 | <Icon :name="GraylogIcon" :size="14" /> |
| 22 | <span>Graylog Query</span> |
| 23 | </div> |
| 24 | <n-button size="tiny" type="primary" secondary @click="showProvisionModal = true"> |
| 25 | <template #icon> |
| 26 | <Icon :name="ProvisionIcon" /> |
| 27 | </template> |
| 28 | Provision Graylog Alert |
| 29 | </n-button> |
| 30 | </div> |
| 31 | </template> |
| 32 | <template #value> |
| 33 | <CodeSource :code="rule.graylog.query" lang="sql" /> |
| 34 | </template> |
| 35 | </CardKV> |
| 36 | |
| 37 | <!-- MITRE ATT&CK --> |
| 38 | <CardKV v-if="rule.tags.mitre_attack_id?.length"> |
| 39 | <template #key>MITRE ATT&CK Techniques</template> |
| 40 | <template #value> |
| 41 | <div class="flex flex-wrap gap-2"> |
| 42 | <Badge v-for="mitre of rule.tags.mitre_attack_id" :key="mitre" color="primary"> |
| 43 | <template #value>{{ mitre }}</template> |
| 44 | </Badge> |
| 45 | </div> |
| 46 | </template> |
| 47 | </CardKV> |
| 48 | |
| 49 | <!-- Data Sources --> |
| 50 | <CardKV v-if="rule.data_source?.length"> |
| 51 | <template #key>Data Sources</template> |
| 52 | <template #value> |
| 53 | <div class="flex flex-wrap gap-2"> |
| 54 | <Badge v-for="source of rule.data_source" :key="source"> |
| 55 | <template #value>{{ source }}</template> |
| 56 | </Badge> |
| 57 | </div> |
| 58 | </template> |
| 59 | </CardKV> |
| 60 | |
| 61 | <!-- Parameters --> |
| 62 | <CardKV v-if="rule.parameters?.length"> |
| 63 | <template #key>Parameters</template> |
| 64 | <template #value> |
| 65 | <div class="grid grid-cols-1 gap-3 py-1 lg:grid-cols-2"> |
| 66 | <CardEntity |
| 67 | v-for="param in rule.parameters" |
| 68 | :key="param.name" |
| 69 | embedded |
| 70 | size="small" |
| 71 | class="h-full" |
| 72 | main-box-class="grow" |
| 73 | card-entity-wrapper-class="h-full" |
| 74 | > |
| 75 | <template #headerMain> |
| 76 | <div class="text-default flex items-center gap-4"> |
| 77 | <div class="text-sm font-semibold">{{ param.name }}</div> |
| 78 | <Badge :color="param.required ? 'danger' : 'success'" type="splitted"> |
| 79 | <template #value> |
| 80 | <span class="text-xs">{{ param.required ? "Required" : "Optional" }}</span> |
| 81 | </template> |
| 82 | </Badge> |
| 83 | </div> |
| 84 | </template> |
| 85 | <template #headerExtra> |
| 86 | <Badge> |
| 87 | <template #value> |
| 88 | <span class="text-xs">{{ param.type }}</span> |
| 89 | </template> |
| 90 | </Badge> |
| 91 | </template> |
| 92 | |
| 93 | <template v-if="param.description" #default> |
| 94 | <p class="text-xs">{{ param.description }}</p> |
| 95 | </template> |
| 96 | |
| 97 | <template #footer> |
| 98 | <div class="flex flex-col gap-1"> |
| 99 | <div |
| 100 | v-if="param.default !== null && param.default !== undefined" |
| 101 | class="text-xs opacity-60" |
| 102 | > |
| 103 | <span class="font-medium">Default:</span> |
| 104 | <code class="code-block ml-1 rounded px-1 py-0.5 text-xs"> |
| 105 | {{ param.default }} |
| 106 | </code> |
| 107 | </div> |
| 108 | <div |
| 109 | v-if="param.example !== null && param.example !== undefined" |
| 110 | class="text-xs opacity-60" |
| 111 | > |
| 112 | <span class="font-medium">Example:</span> |
| 113 | <code class="code-block ml-1 rounded px-1 py-0.5 text-xs"> |
| 114 | {{ param.example }} |
| 115 | </code> |
| 116 | </div> |
| 117 | </div> |
| 118 | </template> |
| 119 | </CardEntity> |
| 120 | </div> |
| 121 | </template> |
| 122 | </CardKV> |
| 123 | |
| 124 | <!-- How to Implement --> |
| 125 | <CardKV v-if="rule.how_to_implement"> |
| 126 | <template #key>How to Implement</template> |
| 127 | <template #value>{{ rule.how_to_implement }}</template> |
| 128 | </CardKV> |
| 129 | |
| 130 | <!-- Known False Positives --> |
| 131 | <CardKV v-if="rule.known_false_positives"> |
| 132 | <template #key>Known False Positives</template> |
| 133 | <template #value>{{ rule.known_false_positives }}</template> |
| 134 | </CardKV> |
| 135 | |
| 136 | <!-- References --> |
| 137 | <CardKV v-if="rule.references?.length"> |
| 138 | <template #key>References</template> |
| 139 | <template #value> |
| 140 | <div class="flex flex-col gap-1"> |
| 141 | <a |
| 142 | v-for="item of rule.references" |
| 143 | :key="item" |
| 144 | :href="item" |
| 145 | target="_blank" |
| 146 | rel="noopener" |
| 147 | class="text-primary-color text-sm hover:underline" |
| 148 | > |
| 149 | {{ item }} |
| 150 | </a> |
| 151 | </div> |
| 152 | </template> |
| 153 | </CardKV> |
| 154 | |
| 155 | <!-- Analytic Stories --> |
| 156 | <div v-if="rule.tags.analytic_story?.length" class="flex flex-wrap gap-2"> |
| 157 | <code v-for="story of rule.tags.analytic_story" :key="story">#{{ story }}</code> |
| 158 | </div> |
| 159 | </div> |
| 160 | |
| 161 | <n-empty v-else-if="!loading" description="Failed to load rule details" /> |
| 162 | |
| 163 | <!-- Provision Graylog Alert Modal --> |
| 164 | <n-modal |
| 165 | v-model:show="showProvisionModal" |
| 166 | preset="card" |
| 167 | :style="{ maxWidth: 'min(550px, 90vw)' }" |
| 168 | title="Provision Graylog Alert" |
| 169 | :bordered="false" |
| 170 | display-directive="show" |
| 171 | segmented |
| 172 | > |
| 173 | <ProvisionGraylogForm |
| 174 | v-if="rule" |
| 175 | :rule-data="rule" |
| 176 | @success="showProvisionModal = false" |
| 177 | @close="showProvisionModal = false" |
| 178 | /> |
| 179 | </n-modal> |
| 180 | </n-spin> |
| 181 | </template> |
| 182 | |
| 183 | <script setup lang="ts"> |
| 184 | import type { RuleDetail } from "@/types/copilotSearches.d" |
| 185 | import _pick from "lodash/pick" |
| 186 | import { NButton, NEmpty, NModal, NSpin, useMessage } from "naive-ui" |
| 187 | import { computed, onBeforeMount, ref } from "vue" |
| 188 | import Api from "@/api" |
| 189 | import Badge from "@/components/common/Badge.vue" |
| 190 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 191 | import CardKV from "@/components/common/cards/CardKV.vue" |
| 192 | import CodeSource from "@/components/common/CodeSource.vue" |
| 193 | import Icon from "@/components/common/Icon.vue" |
| 194 | import PropsList from "@/components/common/PropsList.vue" |
| 195 | import ProvisionGraylogForm from "./ProvisionGraylogForm.vue" |
| 196 | |
| 197 | const props = defineProps<{ |
| 198 | ruleId?: string |
| 199 | ruleData?: RuleDetail |
| 200 | }>() |
| 201 | |
| 202 | const loading = ref(false) |
| 203 | const rule = ref<RuleDetail | null>(null) |
| 204 | const showProvisionModal = ref(false) |
| 205 | const message = useMessage() |
| 206 | |
| 207 | const GraylogIcon = "carbon:notification" |
| 208 | const ProvisionIcon = "carbon:add-alt" |
| 209 | |
| 210 | const infoFields = computed(() => _pick(rule.value, ["author", "version", "date", "status", "type"])) |
| 211 | const riskAssessmentFields = computed(() => ({ |
| 212 | severity: rule.value?.response.severity, |
| 213 | risk_score: rule.value?.response.risk_score, |
| 214 | platform: rule.value?.tags.asset_type, |
| 215 | security_domain: rule.value?.tags.security_domain |
| 216 | })) |
| 217 | |
| 218 | async function loadRule(ruleId: string) { |
| 219 | loading.value = true |
| 220 | try { |
| 221 | const res = await Api.copilotSearches.getRuleById(ruleId) |
| 222 | if (res.data.success) { |
| 223 | rule.value = res.data.rule |
| 224 | } else { |
| 225 | message.error(res.data?.message || "Failed to load rule details") |
| 226 | } |
| 227 | } catch (err: any) { |
| 228 | message.error(err.response?.data?.message || "Failed to load rule details") |
| 229 | } finally { |
| 230 | loading.value = false |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | onBeforeMount(() => { |
| 235 | if (props.ruleData) { |
| 236 | rule.value = props.ruleData |
| 237 | } else if (props.ruleId) { |
| 238 | loadRule(props.ruleId) |
| 239 | } else { |
| 240 | message.error("No rule data or rule ID provided") |
| 241 | } |
| 242 | }) |
| 243 | </script> |