Skip to content

Commit 03d4e86

Browse files
committed
feat[soc-ai]: improve soc-ai integration
1 parent dffce38 commit 03d4e86

23 files changed

Lines changed: 956 additions & 70 deletions

File tree

backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSocAi.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,6 @@ public List<ModuleRequirement> checkRequirements(Long serverId) throws Exception
4545
public List<ModuleConfigurationKey> getConfigurationKeys(Long groupId) throws Exception {
4646
List<ModuleConfigurationKey> keys = new ArrayList<>();
4747

48-
// soc_ai_key
49-
keys.add(ModuleConfigurationKey.builder()
50-
.withGroupId(groupId)
51-
.withConfKey("utmstack.socai.key")
52-
.withConfName("Key")
53-
.withConfDescription("OpenAI Connection key")
54-
.withConfDataType("password")
55-
.withConfRequired(true)
56-
.build());
57-
5848
keys.add(ModuleConfigurationKey.builder()
5949
.withGroupId(groupId)
6050
.withConfKey("utmstack.socai.incidentCreation")

backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupConfigurationService.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Collections;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Objects;
2526
import java.util.stream.Collectors;
2627

2728
/**
@@ -59,11 +60,27 @@ public UtmModule updateConfigurationKeys(Long moduleId, List<UtmModuleGroupConfi
5960
try {
6061
if (CollectionUtils.isEmpty(keys))
6162
throw new ApiException("No configuration keys were provided to update", HttpStatus.BAD_REQUEST);
63+
64+
// Load existing values from DB to detect unchanged encrypted fields
65+
Map<String, String> existingValues = keys.stream()
66+
.filter(k -> k.getId() != null)
67+
.map(k -> moduleConfigurationRepository.findById(k.getId()).orElse(null))
68+
.filter(java.util.Objects::nonNull)
69+
.collect(Collectors.toMap(UtmModuleGroupConfiguration::getConfKey,
70+
c -> c.getConfValue() != null ? c.getConfValue() : "", (a, b) -> a));
71+
6272
for (UtmModuleGroupConfiguration key : keys) {
6373
if (key.getConfRequired() && !StringUtils.hasText(key.getConfValue()))
6474
throw new Exception(String.format("No value was found for required configuration: %1$s (%2$s)", key.getConfName(), key.getConfKey()));
65-
if (key.getConfDataType().equals("password") || key.getConfDataType().equals("file"))
75+
if (key.getConfDataType().equals("password") || key.getConfDataType().equals("file")) {
76+
String existingEncrypted = existingValues.get(key.getConfKey());
77+
// Only encrypt if the value actually changed (is not the same encrypted value from DB)
78+
if (existingEncrypted != null && existingEncrypted.equals(key.getConfValue())) {
79+
// Value unchanged - already encrypted in DB, don't re-encrypt
80+
continue;
81+
}
6682
key.setConfValue(CipherUtil.encrypt(key.getConfValue(), System.getenv(Constants.ENV_ENCRYPTION_KEY)));
83+
}
6784
}
6885
moduleConfigurationRepository.saveAll(keys);
6986

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
6+
7+
<changeSet id="20260409002" author="Yorjander">
8+
<sql>
9+
UPDATE utm_module
10+
SET module_description = 'SOC AI uses advanced language models to investigate Security Operations Center alerts by analyzing large volumes of data, identifying patterns, and providing insights into potential security incidents. By leveraging natural language processing capabilities, security analysts can efficiently triage alerts, prioritize threats, and gather contextual information to aid in incident response and remediation.',
11+
module_icon = 'soc-ai.svg'
12+
WHERE module_name = 'SOC_AI';
13+
14+
UPDATE utm_module_group_configuration
15+
SET conf_visibility = '{"dependsOn": "utmstack.socai.provider", "values": ["custom"]}'
16+
WHERE conf_key = 'utmstack.socai.url';
17+
18+
UPDATE utm_module_group_configuration
19+
SET conf_visibility = '{"dependsOn": "utmstack.socai.provider", "values": ["anthropic", "custom"]}'
20+
WHERE conf_key = 'utmstack.socai.maxTokens';
21+
22+
DELETE FROM utm_module_group_configuration
23+
WHERE conf_key = 'utmstack.socai.key';
24+
</sql>
25+
</changeSet>
26+
</databaseChangeLog>

backend/src/main/resources/config/liquibase/master.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,4 +591,6 @@
591591

592592
<include file="/config/liquibase/changelog/20260409001_add_shell_to_alert_response_rule.xml" relativeToChangelogFile="false"/>
593593

594+
<include file="/config/liquibase/changelog/20260409002_update_socai_config.xml" relativeToChangelogFile="false"/>
595+
594596
</databaseChangeLog>

frontend/src/app/app-module/conf/int-generic-group-config/int-generic-group-config.component.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ import {IntegrationConfigFactory} from './int-config-types/IntegrationConfigFact
2323
templateUrl: './int-generic-group-config.component.html',
2424
styleUrls: ['./int-generic-group-config.component.css']
2525
})
26-
export class IntGenericGroupConfigComponent implements OnInit, OnDestroy {
26+
export class IntGenericGroupConfigComponent implements OnInit, OnChanges, OnDestroy {
2727
@Input() serverId: number;
2828
@Input() moduleId: number;
2929
@Input() groupType = GroupTypeEnum.TENANT;
3030
@Input() allowAdd = true;
3131
@Input() editable = true;
3232
@Input() disablePreAction = false;
33+
@Input() hiddenValues: {[confKey: string]: string} = {};
3334
@Output() configValidChange = new EventEmitter<boolean>();
3435
@Output() runDisablePreAction = new EventEmitter<boolean>();
3536
loading = true;
@@ -57,6 +58,12 @@ export class IntGenericGroupConfigComponent implements OnInit, OnDestroy {
5758
private cdr: ChangeDetectorRef) {
5859
}
5960

61+
ngOnChanges(changes: SimpleChanges) {
62+
if (changes.hiddenValues && !changes.hiddenValues.firstChange && this.groups.length > 0) {
63+
this.applyHiddenValues();
64+
}
65+
}
66+
6067
ngOnInit() {
6168
this.config = this.configFactory.getConfiguration(this.groupType);
6269
this.getGroups().subscribe();
@@ -98,11 +105,29 @@ export class IntGenericGroupConfigComponent implements OnInit, OnDestroy {
98105
return this.config.getIntegrationConfigs(this.moduleId)
99106
.pipe(
100107
tap(() => {
108+
this.applyHiddenValues();
101109
this.configValidChange.emit(this.tenantGroupConfigValid());
102110
this.loading = false;
103111
}));
104112
}
105113

114+
applyHiddenValues() {
115+
if (!this.hiddenValues || Object.keys(this.hiddenValues).length === 0) {
116+
return;
117+
}
118+
for (const group of this.groups) {
119+
for (const conf of group.moduleGroupConfigurations) {
120+
if (this.hiddenValues[conf.confKey] !== undefined) {
121+
const newValue = this.hiddenValues[conf.confKey];
122+
if (conf.confValue !== newValue) {
123+
conf.confValue = newValue;
124+
this.addChange(conf);
125+
}
126+
}
127+
}
128+
}
129+
}
130+
106131
createGroup() {
107132
this.getGroups().subscribe(res => {
108133
const modal = this.modalService.open(IntCreateGroupComponent, {centered: true});
@@ -397,6 +422,10 @@ export class IntGenericGroupConfigComponent implements OnInit, OnDestroy {
397422
}
398423

399424
isVisible(integrationConfig: UtmModuleGroupConfType): boolean {
425+
// Hide fields that are set via hiddenValues
426+
if (this.hiddenValues && this.hiddenValues[integrationConfig.confKey] !== undefined) {
427+
return false;
428+
}
400429

401430
if (!integrationConfig.confVisibility) {
402431
return true;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
.provider-logo {
2+
width: 20px;
3+
height: 20px;
4+
object-fit: contain;
5+
}
6+
7+
.nav-tabs .nav-link {
8+
display: flex;
9+
align-items: center;
10+
padding: 8px 14px;
11+
font-size: 13px;
12+
}
13+
14+
:host ::ng-deep .step-guide a,
15+
:host ::ng-deep p a {
16+
color: #2563eb;
17+
text-decoration: underline;
18+
}
19+
20+
:host ::ng-deep .step-guide a:hover,
21+
:host ::ng-deep p a:hover {
22+
color: #1d4ed8;
23+
}
Lines changed: 165 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,171 @@
11
<div class="w-100 h-100">
22
<div class="card-header d-flex justify-content-between align-items-center">
3-
<h4 class="card-title mb-0 text-primary">
4-
SOC AI
5-
</h4>
3+
<h4 class="card-title mb-0 text-primary">SOC AI</h4>
64
</div>
75
<div class="card-body">
8-
<ol class="setup_list">
9-
<li>
10-
<p class="step-guide mb-3">
11-
<span class="step_number">1</span> Access the API key by logging in to your OpenAI account and navigating to
12-
the
13-
API section
14-
<img alt="IAM configuration" class="step-img"
15-
src="../../../../assets/img/guides/soc-ai/step1.png">
16-
</li>
17-
<li>
18-
<p class="step-guide mb-3">
19-
<span class="step_number">2</span> Create a new API key, and copy the value
20-
</p>
21-
<img alt="IAM configuration" class="step-img"
22-
src="../../../../assets/img/guides/soc-ai/step2.png">
23-
</li>
24-
<li>
25-
<p class="step-guide mb-3">
26-
<span class="step_number">3</span> Paste the API Key in the form below:
27-
</p>
28-
<div class="row mt-3">
29-
<div class="col-lg-12 col-md-12 col-sm-12">
30-
<app-int-generic-group-config [moduleId]="integrationId"
31-
[allowAdd]="false"
32-
[editable]="false"
33-
(configValidChange)="configValidChange($event)"
34-
[serverId]="serverId"></app-int-generic-group-config>
35-
</div>
36-
</div>
37-
38-
</li>
39-
<li>
40-
<p class="step-guide mb-3">
41-
<span class="step_number">4</span>
42-
Click on the button shown below, to activate the UTMStack features related to this integration
43-
</p>
44-
<app-app-module-activate-button [module]="module.SOC_AI" [type]="'integration'"
45-
[disabled]="false"
46-
[serverId]="serverId"
47-
class="mt-3">
48-
</app-app-module-activate-button>
49-
</li>
50-
</ol>
6+
7+
<p class="mb-4">
8+
SOC AI enhances your security operations by integrating advanced language models directly into your alert investigation workflow.
9+
It automatically analyzes alerts, identifies patterns, prioritizes threats, and provides actionable insights to accelerate incident response.
10+
Connect any of the supported AI providers below to get started.
11+
If your provider is not listed, you can use the <strong>Custom</strong> tab to configure any service that supports the OpenAI-compatible chat completions format.
12+
</p>
13+
14+
<div *ngIf="loading" class="d-flex justify-content-center p-5">
15+
<app-utm-spinner [loading]="true" label="Loading configuration..."></app-utm-spinner>
16+
</div>
17+
18+
<ng-container *ngIf="!loading">
19+
<!-- Provider tabs -->
20+
<ngb-tabset (tabChange)="onTabChange($event)">
21+
<ngb-tab *ngFor="let provider of providers" [id]="provider.id">
22+
<ng-template ngbTabTitle>
23+
<img [src]="provider.logo" [alt]="provider.name" class="provider-logo mr-1">
24+
{{ provider.name }}
25+
</ng-template>
26+
<ng-template ngbTabContent>
27+
<div class="mt-3">
28+
<!-- Provider description -->
29+
<p class="step-guide mb-4" [innerHTML]="provider.description"></p>
30+
31+
<!-- Dynamic fields -->
32+
<div class="row">
33+
<ng-container *ngFor="let field of provider.fields">
34+
<!-- Text / Password / Number inputs -->
35+
<div *ngIf="field.type === 'text' || field.type === 'password' || field.type === 'number'"
36+
class="col-lg-6 col-md-12 mb-3">
37+
<label class="pb-1 d-flex justify-content-between align-items-center">
38+
<span>
39+
<i [ngClass]="field.required && !formValues[field.key] ? 'text-danger' : 'text-success'"
40+
class="icon-circle2 mr-1"></i>
41+
{{ field.label }}
42+
</span>
43+
<i *ngIf="field.tooltip" [ngbTooltip]="field.tooltip"
44+
class="icon-question3 text-primary" placement="auto"></i>
45+
</label>
46+
<input [type]="field.type"
47+
[(ngModel)]="formValues[field.key]"
48+
[placeholder]="field.placeholder || ''"
49+
[required]="field.required"
50+
class="form-control">
51+
<small *ngIf="field.required && !formValues[field.key]" class="text-danger">
52+
{{ field.label }} is required
53+
</small>
54+
</div>
55+
56+
<!-- Select -->
57+
<div *ngIf="field.type === 'select'" class="col-lg-6 col-md-12 mb-3">
58+
<label class="pb-1 d-flex justify-content-between align-items-center">
59+
<span>
60+
<i [ngClass]="field.required && !formValues[field.key] ? 'text-danger' : 'text-success'"
61+
class="icon-circle2 mr-1"></i>
62+
{{ field.label }}
63+
</span>
64+
<i *ngIf="field.tooltip" [ngbTooltip]="field.tooltip"
65+
class="icon-question3 text-primary" placement="auto"></i>
66+
</label>
67+
<ng-select *ngIf="formValues[field.key] !== '__custom__'"
68+
[items]="field.options"
69+
bindLabel="label"
70+
bindValue="value"
71+
[(ngModel)]="formValues[field.key]"
72+
[clearable]="false"
73+
[searchable]="false"
74+
[placeholder]="'Select ' + field.label">
75+
</ng-select>
76+
<div *ngIf="formValues[field.key] === '__custom__'" class="input-group">
77+
<input type="text"
78+
[(ngModel)]="customModelValue"
79+
placeholder="Enter model name"
80+
class="form-control">
81+
<div class="input-group-append">
82+
<button class="btn btn-outline-secondary" type="button"
83+
(click)="formValues[field.key] = ''">
84+
<i class="icon-arrow-left32"></i>
85+
</button>
86+
</div>
87+
</div>
88+
<small *ngIf="field.required && !formValues[field.key] && formValues[field.key] !== '__custom__'" class="text-danger">
89+
{{ field.label }} is required
90+
</small>
91+
</div>
92+
93+
<!-- Headers table -->
94+
<div *ngIf="field.type === 'headers' && formValues['authType'] !== 'none'" class="col-12 mb-3">
95+
<label class="pb-1">
96+
<i class="icon-circle2 mr-1 text-success"></i>
97+
{{ field.label }}
98+
</label>
99+
<table class="table table-sm table-bordered mb-2">
100+
<thead>
101+
<tr class="bg-light">
102+
<th style="width: 35%">Header Name</th>
103+
<th>Header Value</th>
104+
<th style="width: 50px"></th>
105+
</tr>
106+
</thead>
107+
<tbody>
108+
<tr *ngFor="let row of headerRows; let i = index">
109+
<td>
110+
<input type="text" [(ngModel)]="row.key"
111+
(ngModelChange)="syncHeadersToForm()"
112+
placeholder="e.g. Authorization"
113+
class="form-control form-control-sm">
114+
</td>
115+
<td>
116+
<input type="text" [(ngModel)]="row.value"
117+
(ngModelChange)="syncHeadersToForm()"
118+
placeholder="e.g. Bearer sk-xxx"
119+
class="form-control form-control-sm">
120+
</td>
121+
<td class="text-center">
122+
<i class="icon-cross2 cursor-pointer text-danger"
123+
(click)="removeHeaderRow(i)"
124+
ngbTooltip="Remove header"></i>
125+
</td>
126+
</tr>
127+
<tr *ngIf="headerRows.length === 0">
128+
<td colspan="3" class="text-center text-muted py-2">
129+
No headers configured. Click "Add Header" to add one.
130+
</td>
131+
</tr>
132+
</tbody>
133+
</table>
134+
<button class="btn btn-sm utm-button utm-button-primary" (click)="addHeaderRow()">
135+
<i class="icon-plus2 mr-1"></i> Add Header
136+
</button>
137+
</div>
138+
139+
<!-- Toggle -->
140+
<div *ngIf="field.type === 'toggle'" class="col-lg-6 col-md-12 mb-3">
141+
<app-utm-toggle
142+
[active]="formValues[field.key] === 'true'"
143+
[label]="field.label"
144+
[emitAtStart]="false"
145+
(toggleChange)="onToggle(field.key, $event)">
146+
</app-utm-toggle>
147+
</div>
148+
</ng-container>
149+
</div>
150+
151+
<!-- Save & Activate -->
152+
<div class="d-flex justify-content-end align-items-center mt-3 border-top pt-3">
153+
<button class="btn utm-button utm-button-primary" (click)="save()"
154+
[disabled]="!isFormValid() || saving">
155+
<i [ngClass]="saving ? 'icon-spinner2 spinner' : 'icon-cog5'" class="mr-1"></i>
156+
Save configuration
157+
</button>
158+
<app-app-module-activate-button [module]="module.SOC_AI" [type]="'integration'"
159+
[disabled]="false"
160+
[serverId]="serverId"
161+
class="ml-2">
162+
</app-app-module-activate-button>
163+
</div>
164+
</div>
165+
</ng-template>
166+
</ngb-tab>
167+
</ngb-tabset>
168+
</ng-container>
169+
51170
</div>
52171
</div>

0 commit comments

Comments
 (0)