Skip to content

Commit 1633610

Browse files
committed
frontend - sigma rulesets list and view
1 parent d50b0f4 commit 1633610

File tree

16 files changed

+724
-62
lines changed

16 files changed

+724
-62
lines changed

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
services:
22
sentinel-kit-app-frontend:
3+
image: node:22-alpine
34
container_name: sentinel-kit-app-frontend
45
hostname: ${SENTINELKIT_FRONTEND_HOSTNAME}
5-
image: node:22-alpine
66
working_dir: /app
77
environment:
88
- VITE_ALLOWED_HOSTS=.${SENTINELKIT_FRONTEND_HOSTNAME}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace App\Controller;
4+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
5+
use Symfony\Component\HttpFoundation\Request;
6+
use Symfony\Component\HttpFoundation\Response;
7+
use Symfony\Component\HttpFoundation\JsonResponse;
8+
use Symfony\Component\Routing\Annotation\Route;
9+
use Symfony\Component\Serializer\SerializerInterface;
10+
use Doctrine\ORM\EntityManagerInterface;
11+
use App\Entity\SigmaRule;
12+
13+
class SigmaController extends AbstractController{
14+
15+
private $entityManger;
16+
private $serializer;
17+
18+
public function __construct(EntityManagerInterface $em, SerializerInterface $serializer){
19+
$this->entityManger = $em;
20+
$this->serializer = $serializer;
21+
}
22+
23+
24+
#[Route('/api/rules/sigma/list', name: 'app_sigma_list', methods: ['GET'])]
25+
public function listRulesSummary(Request $request): Response
26+
{
27+
$rules = $this->entityManger->getRepository(SigmaRule::class)->summaryFindAll();
28+
return new JsonResponse($rules, Response::HTTP_OK);
29+
}
30+
31+
#[Route('/api/rules/sigma/{ruleId}/status', name:'app_sigma_change_rule_status', methods: ['PUT'])]
32+
public function editRuleStatus(Request $request, int $ruleId): Response{
33+
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
34+
if(!$rule){
35+
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);
36+
}
37+
$data = json_decode($request->getContent(), true);
38+
if (!isset($data['active'])) {
39+
return new JsonResponse(['error' => 'Missing active status'], Response::HTTP_BAD_REQUEST);
40+
}
41+
$rule->setActive($data['active']);
42+
$this->entityManger->flush();
43+
return new JsonResponse(['message' => 'Rule status changed successfully'], Response::HTTP_OK);
44+
}
45+
46+
#[Route('api/rules/sigma/{ruleId}/details', name:'app_sigma_get_rule', methods: ['GET'])]
47+
public function getRule(Request $request, int $ruleId): Response {
48+
$rule = $this->entityManger->getRepository(SigmaRule::class)->find($ruleId);
49+
if (!$rule) {
50+
return new JsonResponse(['error' => 'Rule not found'], Response::HTTP_NOT_FOUND);
51+
}
52+
53+
$serializedRule = $this->serializer->serialize($rule, 'json', ['groups' => ['rule_details']]);
54+
return new JsonResponse($serializedRule, Response::HTTP_OK, [], true);
55+
}
56+
}

sentinel-kit_server_backend/src/Entity/SigmaRule.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@
77
use Doctrine\Common\Collections\Collection;
88
use Doctrine\DBAL\Types\Types;
99
use Doctrine\ORM\Mapping as ORM;
10+
use Symfony\Component\Serializer\Annotation\Groups;
1011

1112
#[ORM\Entity(repositoryClass: SigmaRuleRepository::class)]
1213
class SigmaRule
1314
{
1415
#[ORM\Id]
1516
#[ORM\GeneratedValue]
1617
#[ORM\Column]
18+
#[Groups(['rule_details'])]
1719
private ?int $id = null;
1820

1921
#[ORM\Column(length: 255, unique: true)]
22+
#[Groups(['rule_details'])]
2023
private ?string $title = null;
2124

2225
#[ORM\Column(type: Types::TEXT, nullable: true)]
26+
#[Groups(['rule_details'])]
2327
private ?string $description = null;
2428

2529
#[ORM\Column(length: 255, unique: true)]
@@ -32,9 +36,12 @@ class SigmaRule
3236
* @var Collection<int, SigmaRuleVersion>
3337
*/
3438
#[ORM\OneToMany(targetEntity: SigmaRuleVersion::class, mappedBy: 'rule', cascade: ['persist', 'remove'], orphanRemoval: true)]
39+
#[ORM\OrderBy(['createdOn' => 'DESC'])]
40+
#[Groups(['rule_details'])]
3541
private Collection $versions;
3642

3743
#[ORM\Column]
44+
#[Groups(['rule_details'])]
3845
private ?\DateTime $createdOn = null;
3946

4047

sentinel-kit_server_backend/src/Entity/SigmaRuleVersion.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
use App\Repository\SigmaRuleVersionRepository;
66
use Doctrine\DBAL\Types\Types;
77
use Doctrine\ORM\Mapping as ORM;
8+
use Symfony\Component\Serializer\Annotation\Groups;
89

910
#[ORM\Entity(repositoryClass: SigmaRuleVersionRepository::class)]
1011
class SigmaRuleVersion
1112
{
1213
#[ORM\Id]
1314
#[ORM\GeneratedValue]
1415
#[ORM\Column]
16+
#[Groups(['rule_details'])]
1517
private ?int $id = null;
1618

1719
#[ORM\Column(type: Types::TEXT)]
20+
#[Groups(['rule_details'])]
1821
private ?string $content = null;
1922

2023
#[ORM\Column(length: 64, unique: true)]
@@ -25,6 +28,7 @@ class SigmaRuleVersion
2528
private ?SigmaRule $rule = null;
2629

2730
#[ORM\Column]
31+
#[Groups(['rule_details'])]
2832
private ?\DateTime $createdOn = null;
2933

3034
public function __construct()

sentinel-kit_server_backend/src/Repository/SigmaRuleRepository.php

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ public function __construct(ManagerRegistry $registry)
1717
parent::__construct($registry, SigmaRule::class);
1818
}
1919

20-
/**
21-
* Récupère toutes les SigmaRules en joignant uniquement la SigmaRuleVersion la plus récente (basée sur createdOn).
20+
/**
21+
* Retrieve all sigma rules as array object without content
22+
* @return array
23+
*/
24+
public function summaryFindAll() : array{
25+
$qb = $this->createQueryBuilder("r")->orderBy("r.title","ASC")->select("r.id","r.title","r.description","r.active","r.createdOn","r.createdOn");
26+
return $qb->getQuery()->getResult();
27+
}
28+
29+
/**
30+
* Retrieves all SigmaRules joining only the most recent SigmaRuleVersion (based on createdOn).
2231
*
2332
* @return SigmaRule[]
2433
*/
@@ -42,29 +51,4 @@ public function findAllWithLatestRuleVersion(): array
4251
->orderBy('r.title', 'ASC');
4352
return $qb->getQuery()->getResult();
4453
}
45-
46-
// /**
47-
// * @return SigmaRule[] Returns an array of SigmaRule objects
48-
// */
49-
// public function findByExampleField($value): array
50-
// {
51-
// return $this->createQueryBuilder('s')
52-
// ->andWhere('s.exampleField = :val')
53-
// ->setParameter('val', $value)
54-
// ->orderBy('s.id', 'ASC')
55-
// ->setMaxResults(10)
56-
// ->getQuery()
57-
// ->getResult()
58-
// ;
59-
// }
60-
61-
// public function findOneBySomeField($value): ?SigmaRule
62-
// {
63-
// return $this->createQueryBuilder('s')
64-
// ->andWhere('s.exampleField = :val')
65-
// ->setParameter('val', $value)
66-
// ->getQuery()
67-
// ->getOneOrNullResult()
68-
// ;
69-
// }
7054
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
<!doctype html>
1+
<!doctype html data-theme="claude">
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5+
<link rel="stylesheet" href="/src/style.css" />
56
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6-
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet"/>
7+
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet"/>
78
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
89
<title>Sentinel Kit - Admin portal</title>
910
</head>
1011
<body data-theme='claude'>
1112
<div id="app"></div>
1213
<script src="/config.js"></script>
1314
<script type="module" src="/src/main.js"></script>
14-
<script src="../node_modules/flyonui/flyonui.js"></script>
1515
</body>
1616
</html>

sentinel-kit_server_frontend/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12+
"@guolao/vue-monaco-editor": "^1.6.0",
1213
"@tailwindcss/vite": "^4.1.14",
14+
"monaco-editor": "^0.54.0",
15+
"monaco-yaml": "^5.4.0",
1316
"tailwindcss": "^4.1.14",
17+
"vite-plugin-monaco-editor": "^1.1.0",
1418
"vue": "^3.5.22",
19+
"vue-monaco": "^1.2.2",
1520
"vue-router": "^4.6.3"
1621
},
1722
"devDependencies": {

sentinel-kit_server_frontend/src/App.vue

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
</button>
99
</div>
1010
<nav class="flex-grow space-y-2 p-3">
11-
<a v-for="item in menuItems" :key="item.name" href="#" class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition duration-150 group" :class="{ 'justify-center': isCollapsed }" :title="isCollapsed ? item.name : ''">
11+
<RouterLink v-for="item in menuItems" :key="item.name" :to="{ name: item.route }" class="flex items-center p-3 rounded-lg hover:bg-gray-700 transition duration-150 group" :class="{ 'justify-center': isCollapsed }" :title="isCollapsed ? item.name : ''">
1212
<span :class="`w-6 h-6 flex-shrink-0 icon-[${item.icon}] size-10 bg-white`"></span>
1313
<span v-if="!isCollapsed" class="ml-4 font-medium whitespace-nowrap overflow-hidden">
14-
<a href="#" class="link link-primary [--link-color:orange]">{{ item.name }}</a>
14+
<RouterLink :key="item.name" :to="{ name: item.route }" class="link link-primary [--link-color:orange]">{{ item.name }}</RouterLink>
1515
</span>
16-
</a>
16+
</RouterLink>
1717
</nav>
1818
</aside>
1919

@@ -29,7 +29,7 @@
2929

3030
<script setup>
3131
import { onMounted, ref } from 'vue';
32-
import { RouterView } from 'vue-router';
32+
import { RouterView, RouterLink } from 'vue-router';
3333
import Header from './components/Header.vue';
3434
3535
const isLoggedIn = ref(false);
@@ -41,15 +41,12 @@ onMounted(() => {
4141
});
4242
4343
const menuItems = [
44-
{ name: 'Home', icon: 'mdi-light--home' },
45-
{ name: 'Dashboard', icon: 'svg-spinners--blocks-wave' },
46-
{ name: 'Assets & groups', icon: 'line-md--computer-twotone' },
47-
{ name: 'Rulesets', icon: 'mdi--account-child' },
48-
{ name: 'Detections', icon: 'shield' },
49-
{ name: 'Users', icon: 'line-md--account' },
50-
{ name: 'Settings', icon: 'settings' }
44+
{ name: 'Home', icon: 'mdi-light--home', route: 'Home' },
45+
{ name: 'Dashboard', icon: 'svg-spinners--blocks-wave', route: 'Home' },
46+
{ name: 'Assets & groups', icon: 'line-md--computer-twotone', route: 'Home' },
47+
{ name: 'Rulesets', icon: 'mdi--account-child', route: 'RulesList' },
48+
{ name: 'Detections', icon: 'shield', route: 'Home' },
49+
{ name: 'Users', icon: 'line-md--account', route: 'Home' },
50+
{ name: 'Settings', icon: 'settings', route: 'Home' }
5151
];
52-
53-
// Si vous utilisez une fonction de toggle dans le Header, vous pouvez la définir ici:
54-
// const toggleSidebar = () => { isCollapsed.value = !isCollapsed.value; }
5552
</script>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<template>
2+
<div class="rule-summary border-2 border-gray-200 p-4 mb-3 rounded-lg">
3+
4+
<div class="flex justify-between items-center mb-2">
5+
6+
<div class="flex items-center space-x-3">
7+
8+
<RouterLink
9+
:to="{ name: 'RuleEdit', params: { id: props.rule.id } }"
10+
class="btn btn-secondary w-6 h-6"
11+
aria-label="Edit rule"
12+
>
13+
<button
14+
@click="viewDetails"
15+
class="btn btn-primary w-6 h-6"
16+
aria-label="View rule details"
17+
>
18+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
19+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
20+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
21+
</svg>
22+
</button>
23+
</RouterLink>
24+
<h2 class="text-lg font-semibold m-0">
25+
{{ props.rule.title }}
26+
</h2>
27+
</div>
28+
<div class="flex items-center space-x-4">
29+
<div class="relative w-16 h-6 flex items-center justify-end">
30+
31+
<div v-if="isUpdating" class="flex items-center space-x-2">
32+
<div class="w-4 h-4 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
33+
<span class="text-xs text-gray-500">Wait...</span>
34+
</div>
35+
36+
<label v-else class="relative inline-flex items-center cursor-pointer">
37+
<input
38+
type="checkbox"
39+
class="sr-only peer"
40+
:checked="props.rule.active"
41+
@change="toggleStatus"
42+
/>
43+
44+
<div
45+
class="w-11 h-6 bg-gray-300 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"
46+
></div>
47+
</label>
48+
</div>
49+
</div>
50+
</div>
51+
52+
<p class="text-gray-700 text-justify">{{ shortDescription }}</p>
53+
</div>
54+
</template>
55+
56+
<script setup>
57+
import { computed, ref, defineEmits } from 'vue';
58+
import RuleEdit from '../views/RuleEdit.vue';
59+
60+
const emit = defineEmits(['update:ruleStatus', 'showDetails']);
61+
const BASE_URL = import.meta.env.VITE_API_BASE_URL;
62+
63+
const props = defineProps({
64+
rule: {
65+
type: Object,
66+
required: true
67+
}
68+
});
69+
70+
const isUpdating = ref(false);
71+
72+
const shortDescription = computed(() => {
73+
const maxChars = 300;
74+
const description = props.rule.description || '';
75+
76+
if (description.length > maxChars) {
77+
return description.substring(0, maxChars) + '...';
78+
}
79+
return description;
80+
});
81+
82+
const viewDetails = () => {
83+
emit('showDetails', props.rule.id);
84+
};
85+
86+
const toggleStatus = async () => {
87+
if (isUpdating.value) return;
88+
89+
isUpdating.value = true;
90+
const newStatus = !props.rule.active;
91+
92+
try {
93+
const response = await fetch(`${BASE_URL}/rules/sigma/${props.rule.id}/status`, {
94+
method: 'PUT',
95+
headers: {
96+
'Content-Type': 'application/json',
97+
'authorization': `Bearer ${localStorage.getItem('auth_token')}`,
98+
},
99+
body: JSON.stringify({ active: newStatus })
100+
});
101+
102+
if (response.ok) {
103+
props.rule.active = newStatus;
104+
emit('update:ruleStatus', { ruleId: props.rule.id, newStatus: newStatus });
105+
} else {
106+
console.error('API update failed:', await response.text());
107+
}
108+
109+
} catch (error) {
110+
console.error('Network or parsing error:', error);
111+
} finally {
112+
isUpdating.value = false;
113+
}
114+
};
115+
</script>
116+
117+
<style scoped>
118+
.rule-summary h2 {
119+
margin-top: 0;
120+
margin-bottom: 0;
121+
}
122+
</style>

0 commit comments

Comments
 (0)