Skip to content

Commit 79bb466

Browse files
authored
Merge pull request #1301 from utmstack/backlog/fix_build_expression
Backlog/fix build expression
2 parents e61280e + eb5ff78 commit 79bb466

15 files changed

Lines changed: 283 additions & 44 deletions

File tree

frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@
199199
</div>
200200
</div>
201201
</div>
202-
<div class="col-md-12 form-group">
202+
<div class="col-md-12 form-group mt-4">
203203
<app-expression-console
204204
[ngClass]="{'is-invalid': ruleForm.get('definition').invalid && ruleForm.get('definition').touched}"
205205
formControlName="definition"
@@ -260,7 +260,7 @@
260260
</div>
261261
</form>
262262
</div>
263-
<div *ngIf="ruleForm" class="button-container d-flex justify-content-end pr-3 w-100">
263+
<div *ngIf="ruleForm" class="button-container d-flex justify-content-end pb-4 pr-3 w-100">
264264
<button *ngIf="currentStep != RULE_FORM.STEP2" (click)="activeModal.close()"
265265
class="btn utm-button utm-button-grey mr-2">
266266
<i class="icon-cancel-circle2 top-0"></i>&nbsp; Cancel
@@ -274,7 +274,7 @@
274274
Next
275275
<i class="icon-arrow-right32"></i>&nbsp;
276276
</button>
277-
<button *ngIf="currentStep == RULE_FORM.STEP2" (click)="saveRule()" [disabled]="ruleForm.invalid"
277+
<button *ngIf="currentStep == RULE_FORM.STEP2" (click)="saveRule()" [disabled]="!isRuleFormValid"
278278
class="btn utm-button utm-button-primary">
279279
<i [ngClass]="loading ? 'icon-spinner spinner' : 'icon-grid-alt top-0'"></i>&nbsp; Save
280280
</button>

frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {Component, OnDestroy, OnInit} from '@angular/core';
2-
import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
2+
import {AbstractControl, ValidatorFn ,ValidationErrors, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms';
3+
34
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
45
import {forkJoin, Observable} from 'rxjs';
56
import {map} from 'rxjs/operators';
@@ -93,16 +94,47 @@ export class AddRuleComponent implements OnInit, OnDestroy {
9394
this.loadDataTypes();
9495
}
9596

97+
get isRuleFormValid(){
98+
let isValid = true;
99+
if(this.ruleForm.get('afterEvents').errors && this.ruleForm.get('afterEvents').errors.firstElementEmpty) {
100+
Object.keys(this.ruleForm.controls).forEach(controlName => {
101+
if (controlName !== 'afterEvents') {
102+
const control = this.ruleForm.get(controlName);
103+
if (control && !control.valid) {
104+
isValid = false;
105+
}
106+
}
107+
});
108+
}else{
109+
return this.ruleForm.valid
110+
}
111+
112+
return isValid
113+
}
114+
115+
116+
117+
get ruleFormValue(){
118+
if(this.ruleForm.get('afterEvents').errors && this.ruleForm.get('afterEvents').errors.firstElementEmpty) {
119+
return {
120+
...this.ruleForm.value,
121+
afterEvents:[]
122+
}
123+
}else{
124+
return this.ruleForm.value
125+
}
126+
}
127+
96128
saveRule() {
97-
if (this.ruleForm.valid) {
129+
if (this.isRuleFormValid) {
98130
const variables = this.savedVariables .length > 0 ? this.savedVariables.map(variable => ({
99131
as: variable.as,
100132
get: variable.get,
101133
ofType: variable.ofType
102134
})) : [];
103135
this.isSubmitting = true;
104136
const rule: Rule = {
105-
...this.ruleForm.value,
137+
...this.ruleFormValue,
106138
dataTypes: this.getDataTypes(this.ruleForm.value.dataTypes)
107139
};
108140
// rule.definition.ruleVariables = variables;
@@ -146,7 +178,7 @@ export class AddRuleComponent implements OnInit, OnDestroy {
146178
rule && rule.afterEvents && rule.afterEvents.length
147179
? rule.afterEvents.map(event => this.buildSearchRequest(event))
148180
: []
149-
)
181+
,[this.firstEmptySingleElementValidator()])
150182
});
151183
// this.savedVariables = rule ? rule.definition.ruleVariables : [];
152184
const afterEventsArray = this.ruleForm.get('afterEvents') as FormArray;
@@ -162,7 +194,7 @@ export class AddRuleComponent implements OnInit, OnDestroy {
162194
});
163195
});
164196

165-
197+
this.addAfterEvent()
166198
}
167199

168200

@@ -303,4 +335,21 @@ export class AddRuleComponent implements OnInit, OnDestroy {
303335
ngOnDestroy() {
304336
this.dataTypeService.resetTypes();
305337
}
338+
339+
340+
firstEmptySingleElementValidator(): ValidatorFn {
341+
return (control: AbstractControl): ValidationErrors | null => {
342+
if (control instanceof FormArray) {
343+
if (control.length === 1) {
344+
const firstElement = control.at(0).value;
345+
const isEmpty = Object.values(firstElement).every((val:any) => val === '' || val == null || val.length==0);
346+
return !isEmpty ? null : { firstElementEmpty: true };
347+
}
348+
}
349+
return null;
350+
};
351+
}
352+
353+
354+
306355
}

frontend/src/app/rule-management/app-rule/components/expression-console/expression-console.component.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,5 @@
114114
background-color: #f0f0f0;
115115
}
116116

117+
118+

frontend/src/app/rule-management/app-rule/components/import-rules/import-rule.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
</div>
8787

8888
<div *ngIf="rule.showDetail" [ngClass]="{'alert-danger' : !rule.valid, 'alert-success': rule.valid}" class="alert mt-2 py-2 pl-3 w-100">
89-
<app-utm-json-detail-view [rowDocument]="rule"></app-utm-json-detail-view>
89+
<app-utm-json-detail-view [errors]="rule.errors" [rowDocument]="rule"></app-utm-json-detail-view>
9090
</div>
9191
</div>
9292
</div>

frontend/src/app/rule-management/app-rule/components/import-rules/import-rule.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,12 @@ export class ImportRuleComponent implements OnInit, OnDestroy {
145145
}));
146146

147147
this.rules = this.rules.map(rule => {
148-
const isValid = this.importRuleService.isValidRule(rule);
149-
148+
const {isValid,errors} = this.importRuleService.isValidRule(rule);
150149
return {
151150
...rule,
152151
valid: isValid,
153-
status: isValid ? ('valid' as Status) : ('error' as Status)
152+
status: isValid ? ('valid' as Status) : ('error' as Status),
153+
errors
154154
};
155155

156156
});
Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import {Injectable} from '@angular/core';
2-
import {Rule} from '../../../models/rule.model';
1+
import { Injectable } from '@angular/core';
2+
import { Rule } from '../../../models/rule.model';
33

44
@Injectable()
55
export class ImportRuleService {
66

7-
isValidURL(url: string): boolean {
7+
isValidURL(url: string): boolean {
88
try {
99
new URL(url);
1010
return true;
@@ -13,34 +13,101 @@ export class ImportRuleService {
1313
}
1414
}
1515

16-
isValidRule(obj: Rule): obj is Rule {
17-
if (!obj || typeof obj !== 'object') { return false; }
18-
19-
const requiredProps: (keyof Rule)[] = [
20-
'dataTypes', 'name', 'confidentiality', 'integrity', 'availability',
21-
'category', 'technique', 'description', 'references', 'definition', 'adversary'
22-
];
23-
24-
25-
if (!requiredProps.every(prop => prop in obj)) { return false; }
26-
27-
if (
28-
(!Array.isArray(obj.dataTypes) || obj.dataTypes.length === 0) ||
29-
typeof obj.name !== 'string' ||
30-
typeof obj.confidentiality !== 'number' ||
31-
typeof obj.integrity !== 'number' ||
32-
typeof obj.availability !== 'number' ||
33-
typeof obj.category !== 'string' ||
34-
typeof obj.adversary !== 'string' ||
35-
typeof obj.technique !== 'string' ||
36-
typeof obj.description !== 'string' ||
37-
!Array.isArray(obj.references) ||
38-
typeof obj.definition !== 'string'
39-
) {
40-
return false;
16+
private minWordsCheck(value: string, min: number, minLengthPerWord: number): boolean {
17+
if (!value) return false;
18+
const words = value.trim().split(/\s+/).filter(word => word.length >= minLengthPerWord);
19+
return words.length >= min;
20+
21+
}
22+
23+
isValidRule(obj: Rule): { isValid: boolean; errors: Record<string, string[]> } {
24+
const errors: Record<string, string[]> = {};
25+
26+
if (!obj || typeof obj !== 'object') {
27+
return { isValid: false, errors: { rule: ['Rule object is missing or invalid'] } };
28+
}
29+
30+
// dataTypes
31+
if (!Array.isArray(obj.dataTypes) || obj.dataTypes.length === 0) {
32+
errors['dataTypes'] = ['dataTypes are required'];
33+
}
34+
35+
// name
36+
if (typeof obj.name !== 'string' || obj.name.trim() === '') {
37+
errors['name'] = ['Name is required'];
38+
} else if (!this.minWordsCheck(obj.name, 2, 3)) {
39+
errors['name'] = ['Name must contain between 2 and 3 words'];
40+
}
41+
42+
// adversary
43+
if (typeof obj.adversary !== 'string' || obj.adversary.trim() === '') {
44+
errors['adversary'] = ['Adversary is required'];
45+
}
46+
47+
// confidentiality
48+
if (typeof obj.confidentiality !== 'number') {
49+
errors['confidentiality'] = ['Confidentiality must be a number'];
50+
} else if (obj.confidentiality < 0 || obj.confidentiality > 3) {
51+
errors['confidentiality'] = ['Confidentiality must be between 0 and 3'];
52+
}
53+
54+
// integrity
55+
if (typeof obj.integrity !== 'number') {
56+
errors['integrity'] = ['Integrity must be a number'];
57+
} else if (obj.integrity < 0 || obj.integrity > 3) {
58+
errors['integrity'] = ['Integrity must be between 0 and 3'];
59+
}
60+
61+
// availability
62+
if (typeof obj.availability !== 'number') {
63+
errors['availability'] = ['Availability must be a number'];
64+
} else if (obj.availability < 0 || obj.availability > 3) {
65+
errors['availability'] = ['Availability must be between 0 and 3'];
66+
}
67+
68+
// category
69+
if (typeof obj.category !== 'string' || obj.category.trim() === '') {
70+
errors['category'] = ['Category is required'];
71+
} else if (!this.minWordsCheck(obj.category, 1, 3)) {
72+
errors['category'] = ['Category must contain between 1 and 3 words'];
73+
}
74+
75+
// technique
76+
if (typeof obj.technique !== 'string' || obj.technique.trim() === '') {
77+
errors['technique'] = ['Technique is required'];
78+
} else if (!this.minWordsCheck(obj.technique, 2, 3)) {
79+
errors['technique'] = ['Technique must contain between 2 and 3 words'];
4180
}
4281

43-
return obj.references.every((ref: any) => typeof ref === 'string' && this.isValidURL(ref));
82+
// description
83+
if (typeof obj.description !== 'string' || obj.description.trim() === '') {
84+
errors['description'] = ['Description is required'];
85+
} else if (!this.minWordsCheck(obj.description, 2, 3)) {
86+
errors['description'] = ['Description must contain between 2 and 3 words'];
87+
}
88+
89+
// definition
90+
if (typeof obj.definition !== 'string' || obj.definition.trim() === '') {
91+
errors['definition'] = ['Definition is required'];
92+
} else if (!this.minWordsCheck(obj.definition, 2, 3)) {
93+
errors['definition'] = ['Definition must contain between 2 and 3 words'];
94+
}
4495

96+
// references
97+
if (!Array.isArray(obj.references)) {
98+
errors['references'] = ['References must be an array'];
99+
} else {
100+
const invalidRefs = obj.references.filter((ref: any) => typeof ref !== 'string' || !this.isValidURL(ref));
101+
if (invalidRefs.length > 0) {
102+
errors['references'] = ['All references must be valid URLs'];
103+
}
104+
}
105+
106+
107+
return {
108+
isValid: Object.keys(errors).length === 0,
109+
errors
110+
};
45111
}
46112
}
113+

frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@
4141
<td class="td-action">
4242
<div class="d-flex justify-content-end align-items-center medium-icon">
4343

44+
<i (click)="visualizeRule(rule)"
45+
class="icon-eye cursor-pointer ml-2"
46+
ngbTooltip="View Rule"
47+
container="body"
48+
placement="left"></i>
49+
4450
<i (click)="activeRule(rule)"
4551
class="cursor-pointer ml-2"
4652
[ngClass]="{'icon-blocked': rule.ruleActive, 'icon-check2': !rule.ruleActive}"

frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {FilterService} from '../../../services/filter.service';
1919
import {RuleService} from '../../../services/rule.service';
2020
import {AddRuleComponent} from '../add-rule/add-rule.component';
2121
import {ImportRuleComponent} from '../import-rules/import-rule.component';
22+
import {SeeRuleComponent} from '../see-rule/see-rule.component'
2223

2324

2425
@Component({
@@ -203,6 +204,12 @@ export class RuleListComponent implements OnInit, OnDestroy {
203204
this.handleResponse(modal);
204205
}
205206

207+
208+
visualizeRule(rule:Rule){
209+
const modal = this.modalService.open(SeeRuleComponent, {size: 'lg', centered: true});
210+
modal.componentInstance.rowDocument = rule;
211+
}
212+
206213
handleResponse(modal: NgbModalRef) {
207214
modal.result.then((result: boolean) => {
208215
if (result) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
<app-utm-modal-header [name]="'Rule Details'"></app-utm-modal-header>
3+
4+
5+
<!-- utm-json-detail-view.component.html -->
6+
<div class="json-detail-container p-4">
7+
<div class="header d-flex flex-row w-100 align-items-center justify-content-end">
8+
<button class="btn utm-button utm-button-primary" (click)="copyToClipboard()" title="Copy YAML">
9+
<i class="icon-clipboard"></i>Copy
10+
</button>
11+
<small *ngIf="copied" class="copied-msg">✅ Copied!</small>
12+
13+
</div>
14+
<app-utm-json-detail-view [rowDocument]="rowDocument"></app-utm-json-detail-view>
15+
</div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
/* utm-json-detail-view.component.scss */
3+
.json-detail-container {
4+
display: flex;
5+
flex-direction: column;
6+
gap: 8px;
7+
}
8+
9+
10+
.copy-btn {
11+
border: none;
12+
background: transparent;
13+
cursor: pointer;
14+
font-size: 18px;
15+
}
16+
17+
.copied-msg {
18+
color: green;
19+
font-size: 12px;
20+
}

0 commit comments

Comments
 (0)