Skip to content

Commit 88f3dee

Browse files
author
csavelief
committed
Backport MEMEX
1 parent ba9df0f commit 88f3dee

4 files changed

Lines changed: 154 additions & 127 deletions

File tree

app/Http/Controllers/CyberBuddyNextGenController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public function converse(ConverseRequest $request, bool $fallbackOnNextCollectio
9595
$openPorts = $fnOpenPorts->text();
9696

9797
$notes = TimelineItem::fetchNotes($user->id, null, null, 0)
98-
->map(fn(TimelineItem $note) => "- {$note->timestamp->format('Y-m-d H:i:s')} : {$note->attributes()['content']}")
98+
->map(fn(TimelineItem $note) => "- {$note->timestamp->format('Y-m-d H:i:s')} : {$note->attributes()['body']}")
9999
->join("\n");
100100

101101
// Load the prompt

app/Jobs/ProcessIncomingEmails.php

Lines changed: 149 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Models\Invitation;
1111
use App\Models\Role;
1212
use App\Models\Tenant;
13+
use App\Models\TimelineItem;
1314
use App\Models\YnhFramework;
1415
use App\Rules\IsValidCollectionName;
1516
use App\User;
@@ -31,7 +32,8 @@ class ProcessIncomingEmails implements ShouldQueue
3132
{
3233
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
3334

34-
const string SENDER = 'cyberbuddy@cywise.io';
35+
const string SENDER_CYBERBUDDY = 'cyberbuddy@cywise.io';
36+
const string SENDER_MEMEX = 'memex@cywise.io';
3537

3638
public $tries = 1;
3739
public $maxExceptions = 1;
@@ -69,8 +71,10 @@ public function handle()
6971
$from = $message->getFrom()->all();
7072
$cc = $message->getCc()->all();
7173
$bcc = $message->getBcc()->all();
74+
$isCyberBuddy = collect($to)->contains(self::SENDER_CYBERBUDDY);
75+
$isMemex = collect($to)->contains(self::SENDER_MEMEX);
7276

73-
if (!collect($to)->contains(self::SENDER)) {
77+
if (!$isCyberBuddy && !$isMemex) {
7478
continue;
7579
}
7680
if (count($from) !== 1) {
@@ -84,48 +88,7 @@ public function handle()
8488
$address = $from[0];
8589

8690
// Create shadow profile
87-
/** @var User $user */
88-
$user = User::where('email', $address->mail)->first();
89-
if ($user) {
90-
Auth::login($user); // otherwise the tenant will not be properly set
91-
} else {
92-
/** @var Invitation $invitation */
93-
$invitation = Invitation::where('email', $address->mail)->first();
94-
95-
if (!$invitation) {
96-
$invitation = InvitationProxy::createInvitation($address->mail, "J. Doe");
97-
}
98-
99-
/** @var Tenant $tenant */
100-
$tenant = Tenant::create(['name' => Str::random()]);
101-
$user = $invitation->createUser([
102-
'password' => Str::random(64),
103-
'tenant_id' => $tenant->id,
104-
'type' => UserType::CLIENT(),
105-
'terms_accepted' => true,
106-
]);
107-
108-
$user->syncRoles(Role::ADMINISTRATOR, Role::LIMITED_ADMINISTRATOR, Role::BASIC_END_USER);
109-
110-
Auth::login($user); // otherwise the tenant will not be properly set
111-
112-
// Create shadow collections for some frameworks
113-
$frameworks = \App\Models\YnhFramework::all();
114-
115-
foreach ($frameworks as $framework) {
116-
if ($framework->file === 'seeds/frameworks/anssi/anssi-genai-security-recommendations-1.0.jsonl') {
117-
$this->importFramework($framework, 20);
118-
} else if ($framework->file === 'seeds/frameworks/anssi/anssi-guide-hygiene-detail.jsonl') {
119-
$this->importFramework($framework, 10);
120-
} else if ($framework->file === 'seeds/frameworks/gdpr/gdpr.jsonl') {
121-
$this->importFramework($framework, 30);
122-
} else if ($framework->file === 'seeds/frameworks/dora/dora.jsonl') {
123-
$this->importFramework($framework, 50);
124-
} else if ($framework->file === 'seeds/frameworks/nis2/nis2-directive.jsonl') {
125-
$this->importFramework($framework, 40);
126-
}
127-
}
128-
}
91+
$user = $this->getOrCreateUser($address->mail);
12992

13093
// Ensure all prompts are properly loaded
13194
/* if (Prompt::count() >= 4) {
@@ -138,85 +101,10 @@ public function handle()
138101
if (File::where('is_deleted', false)->get()->contains(fn(File $file) => !$file->is_embedded)) {
139102
Log::warning($message->getSubject());
140103
Log::warning("Some collections are not ready yet. Skipping email processing for now.");
141-
continue;
142-
}
143-
144-
// Extract the thread id in order to be able to load the existing conversation
145-
// If the thread id cannot be found, a new conversation is created
146-
$threadId = null;
147-
$matches = [];
148-
preg_match_all("/\s*thread_id=(?<threadid>[a-zA-Z0-9]{10})\s*/i", $message->getTextBody(), $matches, PREG_SET_ORDER);
149-
150-
foreach ($matches as $match) {
151-
if (!empty($match['threadid'])) {
152-
$threadId = $match['threadid'];
153-
break;
154-
}
155-
}
156-
if (empty($threadId)) {
157-
$threadId = Str::random(10);
158-
}
159-
160-
/** @var Conversation $conversation */
161-
$conversation = Conversation::where('thread_id', $threadId)
162-
->where('format', Conversation::FORMAT_V1)
163-
->where('created_by', $user?->id)
164-
->first();
165-
166-
$conversation = $conversation ?? Conversation::create([
167-
'thread_id' => $threadId,
168-
'dom' => json_encode([]),
169-
'autosaved' => true,
170-
'created_by' => $user?->id,
171-
'format' => Conversation::FORMAT_V1,
172-
]);
173-
174-
// Remove previous messages i.e. rows starting with >
175-
$body = trim(preg_replace("/^(>.*)|(On\s+.*\s+wrote:)[\n\r]?$/im", '', $message->getTextBody()));
176-
177-
Log::debug('subject=' . $message->getSubject()->all()[0]);
178-
Log::debug('body=' . $body);
179-
180-
// Call CyberBuddy
181-
$request = new ConverseRequest();
182-
$request->replace([
183-
'thread_id' => $threadId,
184-
'directive' => $body,
185-
]);
186-
187-
$controller = new CyberBuddyNextGenController();
188-
$response = $controller->converse($request, true);
189-
$json = json_decode($response->content(), true);
190-
$subject = $message->getSubject()[0];
191-
$body = $json['answer']['html'] ?? '';
192-
193-
EndVulnsScanListener::sendEmail(
194-
self::SENDER,
195-
$address->mail,
196-
"Re: {$subject}",
197-
"CyberBuddy vous répond !",
198-
"
199-
{$body}
200-
<p>Pour importer tes propres documents et profiter pleinement des capacités de CyberBuddy, ton assistant Cyber, finalise ton inscription à Cywise :</p>
201-
",
202-
route('password.reset', [
203-
'token' => app(PasswordBroker::class)->createToken($user),
204-
'email' => $user->email,
205-
'reason' => 'Finalisez votre inscription en créant un mot de passe',
206-
'action' => 'Créer mon mot de passe',
207-
]),
208-
"je me connecte à Cywise",
209-
"
210-
<p>Je reste à ta disposition pour toute question ou assistance supplémentaire. Merci encore pour ta confiance en Cywise !</p>
211-
<p>Bien à toi,</p>
212-
<p>CyberBuddy</p>
213-
<span style='color:white'>thread_id={$threadId}</span>
214-
",
215-
);
216-
217-
if (!$message->move('CyberBuddy')) {
218-
Log::error($message->getSubject());
219-
Log::error('Message could not be moved!');
104+
} else if ($isCyberBuddy) {
105+
$this->cyberBuddy($user, $message);
106+
} else if ($isMemex) {
107+
$this->memex($user, $message);
220108
}
221109
}
222110
}
@@ -228,6 +116,54 @@ public function handle()
228116
}
229117
}
230118

119+
private function getOrCreateUser(string $email): User
120+
{
121+
/** @var User $user */
122+
$user = User::where('email', $email)->first();
123+
if ($user) {
124+
Auth::login($user); // otherwise the tenant will not be properly set
125+
} else {
126+
127+
/** @var Invitation $invitation */
128+
$invitation = Invitation::where('email', $email)->first();
129+
130+
if (!$invitation) {
131+
$invitation = InvitationProxy::createInvitation($email, Str::before($email, '@'));
132+
}
133+
134+
/** @var Tenant $tenant */
135+
$tenant = Tenant::create(['name' => Str::random()]);
136+
$user = $invitation->createUser([
137+
'password' => Str::random(64),
138+
'tenant_id' => $tenant->id,
139+
'type' => UserType::CLIENT(),
140+
'terms_accepted' => true,
141+
]);
142+
143+
$user->syncRoles(Role::ADMINISTRATOR, Role::LIMITED_ADMINISTRATOR, Role::BASIC_END_USER);
144+
145+
Auth::login($user); // otherwise the tenant will not be properly set
146+
147+
// Create shadow collections for some frameworks
148+
$frameworks = \App\Models\YnhFramework::all();
149+
150+
foreach ($frameworks as $framework) {
151+
if ($framework->file === 'seeds/frameworks/anssi/anssi-genai-security-recommendations-1.0.jsonl') {
152+
$this->importFramework($framework, 20);
153+
} else if ($framework->file === 'seeds/frameworks/anssi/anssi-guide-hygiene-detail.jsonl') {
154+
$this->importFramework($framework, 10);
155+
} else if ($framework->file === 'seeds/frameworks/gdpr/gdpr.jsonl') {
156+
$this->importFramework($framework, 30);
157+
} else if ($framework->file === 'seeds/frameworks/dora/dora.jsonl') {
158+
$this->importFramework($framework, 50);
159+
} else if ($framework->file === 'seeds/frameworks/nis2/nis2-directive.jsonl') {
160+
$this->importFramework($framework, 40);
161+
}
162+
}
163+
}
164+
return $user;
165+
}
166+
231167
private function importFramework(YnhFramework $framework, int $priority): void
232168
{
233169
/** @var \App\Models\Collection $collection */
@@ -256,4 +192,94 @@ private function importFramework(YnhFramework $framework, int $priority): void
256192
);
257193
$url = \App\Http\Controllers\CyberBuddyController::saveUploadedFile($collection, $file);
258194
}
195+
196+
private function cyberBuddy(User $user, \Webklex\PHPIMAP\Message $message)
197+
{
198+
// Extract the thread id in order to be able to load the existing conversation
199+
// If the thread id cannot be found, a new conversation is created
200+
$threadId = null;
201+
$matches = [];
202+
preg_match_all("/\s*thread_id=(?<threadid>[a-zA-Z0-9]{10})\s*/i", $message->getTextBody(), $matches, PREG_SET_ORDER);
203+
204+
foreach ($matches as $match) {
205+
if (!empty($match['threadid'])) {
206+
$threadId = $match['threadid'];
207+
break;
208+
}
209+
}
210+
if (empty($threadId)) {
211+
$threadId = Str::random(10);
212+
}
213+
214+
/** @var Conversation $conversation */
215+
$conversation = Conversation::where('thread_id', $threadId)
216+
->where('format', Conversation::FORMAT_V1)
217+
->where('created_by', $user->id)
218+
->first();
219+
220+
$conversation = $conversation ?? Conversation::create([
221+
'thread_id' => $threadId,
222+
'dom' => json_encode([]),
223+
'autosaved' => true,
224+
'created_by' => $user->id,
225+
'format' => Conversation::FORMAT_V1,
226+
]);
227+
228+
// Remove previous messages i.e. rows starting with >
229+
$body = trim(preg_replace("/^(>.*)|(On\s+.*\s+wrote:)[\n\r]?$/im", '', $message->getTextBody()));
230+
231+
Log::debug('subject=' . $message->getSubject()[0] ?? '');
232+
Log::debug('body=' . $body);
233+
234+
// Call CyberBuddy
235+
$request = new ConverseRequest();
236+
$request->replace([
237+
'thread_id' => $threadId,
238+
'directive' => $body,
239+
]);
240+
241+
$controller = new CyberBuddyNextGenController();
242+
$response = $controller->converse($request, true);
243+
$json = json_decode($response->content(), true);
244+
$subject = $message->getSubject()[0] ?? '';
245+
$body = $json['answer']['html'] ?? '';
246+
247+
EndVulnsScanListener::sendEmail(
248+
self::SENDER_CYBERBUDDY,
249+
$user->email,
250+
"Re: {$subject}",
251+
"CyberBuddy vous répond !",
252+
"
253+
{$body}
254+
<p>Pour importer tes propres documents et profiter pleinement des capacités de CyberBuddy, ton assistant Cyber, finalise ton inscription à Cywise :</p>
255+
",
256+
route('password.reset', [
257+
'token' => app(PasswordBroker::class)->createToken($user),
258+
'email' => $user->email,
259+
'reason' => 'Finalisez votre inscription en créant un mot de passe',
260+
'action' => 'Créer mon mot de passe',
261+
]),
262+
"je me connecte à Cywise",
263+
"
264+
<p>Je reste à ta disposition pour toute question ou assistance supplémentaire. Merci encore pour ta confiance en Cywise !</p>
265+
<p>Bien à toi,</p>
266+
<p>CyberBuddy</p>
267+
<span style='color:white'>thread_id={$threadId}</span>
268+
",
269+
);
270+
271+
if (!$message->move('CyberBuddy')) {
272+
Log::error($message->getSubject());
273+
Log::error('Message could not be moved to the CyberBuddy folder!');
274+
}
275+
}
276+
277+
private function memex(User $user, \Webklex\PHPIMAP\Message $message)
278+
{
279+
$item = TimelineItem::createNote($user->id, $message->getTextBody(), $message->getSubject()[0] ?? '');
280+
if (!$message->move('Memex')) {
281+
Log::error($message->getSubject());
282+
Log::error('Message could not be moved to the Memex folder!');
283+
}
284+
}
259285
}

app/Models/TimelineItem.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ class TimelineItem extends Model
4242
'updated_at' => 'datetime',
4343
];
4444

45-
public static function createNote(int $ownedBy, string $content): TimelineItem
45+
public static function createNote(int $ownedBy, string $body, string $subject = ''): TimelineItem
4646
{
4747
return self::createItem($ownedBy, 'note', Carbon::now(), 0, [
48-
'content' => Str::limit(trim($content), 1000 - 3, '...'),
48+
'body' => Str::limit(trim($body), 1000 - 3, '...'),
49+
'subject' => Str::limit(trim($subject), 1000 - 3, '...'),
4950
]);
5051
}
5152

resources/views/cywise/_timeline-item-note.blade.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class="icon icon-tabler icons-tabler-outline icon-tabler-notes">
2121
</span>
2222
</div>
2323
<div class="comment">
24-
{{ $note->attributes()['content'] ?? '' }}
24+
{{ $note->attributes()['body'] ?? '' }}
2525
</div>
2626
<div style="display: flex; gap: 10px;">
2727
<button class="show-replies" title="{{ __('Delete') }}" onclick="deleteNote('{{ $note->id }}')">

0 commit comments

Comments
 (0)