@serve.zone/dcrouter 13.23.0 → 13.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +952 -792
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/opsserver/handlers/security.handler.js +22 -1
- package/dist_ts/security/classes.security-policy-manager.d.ts +7 -2
- package/dist_ts/security/classes.security-policy-manager.js +26 -5
- package/dist_ts_interfaces/requests/security-policy.d.ts +32 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +28 -0
- package/dist_ts_web/appstate.js +171 -4
- package/dist_ts_web/elements/network/ops-view-network-activity.d.ts +9 -0
- package/dist_ts_web/elements/network/ops-view-network-activity.js +210 -3
- package/dist_ts_web/elements/security/ops-view-security-blocked.d.ts +12 -3
- package/dist_ts_web/elements/security/ops-view-security-blocked.js +407 -52
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/opsserver/handlers/security.handler.ts +38 -0
- package/ts/security/classes.security-policy-manager.ts +29 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +236 -3
- package/ts_web/elements/network/ops-view-network-activity.ts +219 -2
- package/ts_web/elements/security/ops-view-security-blocked.ts +414 -51
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
IIpIntelligenceRecord,
|
|
6
6
|
ISecurityBlockRule,
|
|
7
7
|
ISecurityCompiledPolicy,
|
|
8
|
+
ISecurityPolicyAuditEvent,
|
|
8
9
|
TSecurityBlockRuleMatchMode,
|
|
9
10
|
TSecurityBlockRuleType,
|
|
10
11
|
} from '../../ts_interfaces/data/security-policy.js';
|
|
@@ -44,7 +45,7 @@ export class SecurityPolicyManager {
|
|
|
44
45
|
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
public async observeIp(ipAddress: string): Promise<void> {
|
|
48
|
+
public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
|
|
48
49
|
const ip = this.normalizeIp(ipAddress);
|
|
49
50
|
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
|
|
50
51
|
return;
|
|
@@ -54,7 +55,7 @@ export class SecurityPolicyManager {
|
|
|
54
55
|
try {
|
|
55
56
|
const now = Date.now();
|
|
56
57
|
let doc = await IpIntelligenceDoc.findByIp(ip);
|
|
57
|
-
if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
|
58
|
+
if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
|
58
59
|
if (now - doc.lastSeenAt > 60_000) {
|
|
59
60
|
doc.lastSeenAt = now;
|
|
60
61
|
doc.seenCount = (doc.seenCount || 0) + 1;
|
|
@@ -90,7 +91,31 @@ export class SecurityPolicyManager {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
|
|
93
|
-
return (await IpIntelligenceDoc.findAll()).map((doc) => (
|
|
94
|
+
return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
|
|
98
|
+
const ip = this.normalizeIp(ipAddress);
|
|
99
|
+
if (!ip || !this.isPublicIp(ip)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
await this.observeIp(ip, { force: true });
|
|
103
|
+
const doc = await IpIntelligenceDoc.findByIp(ip);
|
|
104
|
+
return doc ? this.intelligenceFromDoc(doc) : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
|
|
108
|
+
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
|
|
109
|
+
id: doc.id,
|
|
110
|
+
action: doc.action,
|
|
111
|
+
actor: doc.actor,
|
|
112
|
+
details: doc.details,
|
|
113
|
+
createdAt: doc.createdAt,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private intelligenceFromDoc(doc: IpIntelligenceDoc): IIpIntelligenceRecord {
|
|
118
|
+
return {
|
|
94
119
|
ipAddress: doc.ipAddress,
|
|
95
120
|
asn: doc.asn,
|
|
96
121
|
asnOrg: doc.asnOrg,
|
|
@@ -109,7 +134,7 @@ export class SecurityPolicyManager {
|
|
|
109
134
|
lastSeenAt: doc.lastSeenAt,
|
|
110
135
|
updatedAt: doc.updatedAt,
|
|
111
136
|
seenCount: doc.seenCount,
|
|
112
|
-
}
|
|
137
|
+
};
|
|
113
138
|
}
|
|
114
139
|
|
|
115
140
|
public async createBlockRule(input: {
|
package/ts_web/appstate.ts
CHANGED
|
@@ -54,6 +54,7 @@ export interface INetworkState {
|
|
|
54
54
|
topIPs: Array<{ ip: string; count: number }>;
|
|
55
55
|
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
|
56
56
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
|
57
|
+
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
|
57
58
|
domainActivity: interfaces.data.IDomainActivity[];
|
|
58
59
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
|
59
60
|
requestsPerSecond: number;
|
|
@@ -66,6 +67,16 @@ export interface INetworkState {
|
|
|
66
67
|
error: string | null;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
export interface ISecurityPolicyState {
|
|
71
|
+
rules: interfaces.data.ISecurityBlockRule[];
|
|
72
|
+
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
|
73
|
+
compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
|
|
74
|
+
auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
|
|
75
|
+
isLoading: boolean;
|
|
76
|
+
error: string | null;
|
|
77
|
+
lastUpdated: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
export interface ICertificateState {
|
|
70
81
|
certificates: interfaces.requests.ICertificateInfo[];
|
|
71
82
|
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
|
@@ -164,6 +175,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|
|
164
175
|
topIPs: [],
|
|
165
176
|
topIPsByBandwidth: [],
|
|
166
177
|
throughputByIP: [],
|
|
178
|
+
ipIntelligence: [],
|
|
167
179
|
domainActivity: [],
|
|
168
180
|
throughputHistory: [],
|
|
169
181
|
requestsPerSecond: 0,
|
|
@@ -178,6 +190,20 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|
|
178
190
|
'soft'
|
|
179
191
|
);
|
|
180
192
|
|
|
193
|
+
export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
|
|
194
|
+
'securityPolicy',
|
|
195
|
+
{
|
|
196
|
+
rules: [],
|
|
197
|
+
ipIntelligence: [],
|
|
198
|
+
compiledPolicy: null,
|
|
199
|
+
auditEvents: [],
|
|
200
|
+
isLoading: false,
|
|
201
|
+
error: null,
|
|
202
|
+
lastUpdated: 0,
|
|
203
|
+
},
|
|
204
|
+
'soft',
|
|
205
|
+
);
|
|
206
|
+
|
|
181
207
|
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
|
182
208
|
'emailOps',
|
|
183
209
|
{
|
|
@@ -517,9 +543,18 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
517
543
|
interfaces.requests.IReq_GetNetworkStats
|
|
518
544
|
>('/typedrequest', 'getNetworkStats');
|
|
519
545
|
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
546
|
+
const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
547
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
548
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
549
|
+
|
|
550
|
+
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
|
|
551
|
+
networkStatsRequest.fire({
|
|
552
|
+
identity: context.identity,
|
|
553
|
+
}),
|
|
554
|
+
ipIntelligenceRequest.fire({
|
|
555
|
+
identity: context.identity,
|
|
556
|
+
}),
|
|
557
|
+
]);
|
|
523
558
|
|
|
524
559
|
// Use the connections data for the connection list
|
|
525
560
|
// and network stats for throughput and IP analytics
|
|
@@ -561,6 +596,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
561
596
|
topIPs: networkStatsResponse.topIPs || [],
|
|
562
597
|
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
|
563
598
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
|
599
|
+
ipIntelligence: ipIntelligenceResponse.records || [],
|
|
564
600
|
domainActivity: networkStatsResponse.domainActivity || [],
|
|
565
601
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
|
566
602
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
|
@@ -582,6 +618,182 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
582
618
|
}
|
|
583
619
|
});
|
|
584
620
|
|
|
621
|
+
// ============================================================================
|
|
622
|
+
// Security Policy Actions
|
|
623
|
+
// ============================================================================
|
|
624
|
+
|
|
625
|
+
export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
626
|
+
async (statePartArg): Promise<ISecurityPolicyState> => {
|
|
627
|
+
const context = getActionContext();
|
|
628
|
+
const currentState = statePartArg.getState()!;
|
|
629
|
+
if (!context.identity) return currentState;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
633
|
+
interfaces.requests.IReq_ListSecurityBlockRules
|
|
634
|
+
>('/typedrequest', 'listSecurityBlockRules');
|
|
635
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
636
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
637
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
638
|
+
const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
639
|
+
interfaces.requests.IReq_GetCompiledSecurityPolicy
|
|
640
|
+
>('/typedrequest', 'getCompiledSecurityPolicy');
|
|
641
|
+
const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
642
|
+
interfaces.requests.IReq_ListSecurityPolicyAudit
|
|
643
|
+
>('/typedrequest', 'listSecurityPolicyAudit');
|
|
644
|
+
|
|
645
|
+
const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
|
|
646
|
+
rulesRequest.fire({ identity: context.identity }),
|
|
647
|
+
intelligenceRequest.fire({ identity: context.identity }),
|
|
648
|
+
compiledPolicyRequest.fire({ identity: context.identity }),
|
|
649
|
+
auditRequest.fire({ identity: context.identity, limit: 100 }),
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
rules: rulesResponse.rules || [],
|
|
654
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
655
|
+
compiledPolicy: compiledPolicyResponse.policy,
|
|
656
|
+
auditEvents: auditResponse.events || [],
|
|
657
|
+
isLoading: false,
|
|
658
|
+
error: null,
|
|
659
|
+
lastUpdated: Date.now(),
|
|
660
|
+
};
|
|
661
|
+
} catch (error: unknown) {
|
|
662
|
+
return {
|
|
663
|
+
...currentState,
|
|
664
|
+
isLoading: false,
|
|
665
|
+
error: error instanceof Error ? error.message : 'Failed to fetch security policy',
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
|
672
|
+
type: interfaces.data.TSecurityBlockRuleType;
|
|
673
|
+
value: string;
|
|
674
|
+
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
|
675
|
+
reason?: string;
|
|
676
|
+
enabled?: boolean;
|
|
677
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
|
678
|
+
const context = getActionContext();
|
|
679
|
+
const currentState = statePartArg.getState()!;
|
|
680
|
+
if (!context.identity) return currentState;
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
684
|
+
interfaces.requests.IReq_CreateSecurityBlockRule
|
|
685
|
+
>('/typedrequest', 'createSecurityBlockRule');
|
|
686
|
+
|
|
687
|
+
const response = await request.fire({
|
|
688
|
+
identity: context.identity,
|
|
689
|
+
type: dataArg.type,
|
|
690
|
+
value: dataArg.value,
|
|
691
|
+
matchMode: dataArg.matchMode,
|
|
692
|
+
reason: dataArg.reason,
|
|
693
|
+
enabled: dataArg.enabled,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
if (!response.success) {
|
|
697
|
+
return { ...currentState, error: response.message || 'Failed to create security block rule' };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
701
|
+
} catch (error: unknown) {
|
|
702
|
+
return {
|
|
703
|
+
...currentState,
|
|
704
|
+
error: error instanceof Error ? error.message : 'Failed to create security block rule',
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
|
710
|
+
id: string;
|
|
711
|
+
value?: string;
|
|
712
|
+
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
|
713
|
+
reason?: string;
|
|
714
|
+
enabled?: boolean;
|
|
715
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
|
716
|
+
const context = getActionContext();
|
|
717
|
+
const currentState = statePartArg.getState()!;
|
|
718
|
+
if (!context.identity) return currentState;
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
722
|
+
interfaces.requests.IReq_UpdateSecurityBlockRule
|
|
723
|
+
>('/typedrequest', 'updateSecurityBlockRule');
|
|
724
|
+
|
|
725
|
+
const response = await request.fire({
|
|
726
|
+
identity: context.identity,
|
|
727
|
+
id: dataArg.id,
|
|
728
|
+
value: dataArg.value,
|
|
729
|
+
matchMode: dataArg.matchMode,
|
|
730
|
+
reason: dataArg.reason,
|
|
731
|
+
enabled: dataArg.enabled,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
if (!response.success) {
|
|
735
|
+
return { ...currentState, error: response.message || 'Failed to update security block rule' };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
739
|
+
} catch (error: unknown) {
|
|
740
|
+
return {
|
|
741
|
+
...currentState,
|
|
742
|
+
error: error instanceof Error ? error.message : 'Failed to update security block rule',
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
|
|
748
|
+
async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
|
|
749
|
+
const context = getActionContext();
|
|
750
|
+
const currentState = statePartArg.getState()!;
|
|
751
|
+
if (!context.identity) return currentState;
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
755
|
+
interfaces.requests.IReq_DeleteSecurityBlockRule
|
|
756
|
+
>('/typedrequest', 'deleteSecurityBlockRule');
|
|
757
|
+
|
|
758
|
+
const response = await request.fire({ identity: context.identity, id: ruleId });
|
|
759
|
+
if (!response.success) {
|
|
760
|
+
return { ...currentState, error: response.message || 'Failed to delete security block rule' };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
764
|
+
} catch (error: unknown) {
|
|
765
|
+
return {
|
|
766
|
+
...currentState,
|
|
767
|
+
error: error instanceof Error ? error.message : 'Failed to delete security block rule',
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
|
|
774
|
+
async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
|
|
775
|
+
const context = getActionContext();
|
|
776
|
+
const currentState = statePartArg.getState()!;
|
|
777
|
+
if (!context.identity) return currentState;
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
781
|
+
interfaces.requests.IReq_RefreshIpIntelligence
|
|
782
|
+
>('/typedrequest', 'refreshIpIntelligence');
|
|
783
|
+
const response = await request.fire({ identity: context.identity, ipAddress });
|
|
784
|
+
if (!response.success) {
|
|
785
|
+
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
|
786
|
+
}
|
|
787
|
+
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
788
|
+
} catch (error: unknown) {
|
|
789
|
+
return {
|
|
790
|
+
...currentState,
|
|
791
|
+
error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
);
|
|
796
|
+
|
|
585
797
|
// ============================================================================
|
|
586
798
|
// Email Operations Actions
|
|
587
799
|
// ============================================================================
|
|
@@ -2665,6 +2877,27 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
2665
2877
|
isLoading: false,
|
|
2666
2878
|
error: null,
|
|
2667
2879
|
});
|
|
2880
|
+
|
|
2881
|
+
try {
|
|
2882
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
2883
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
2884
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
2885
|
+
const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity });
|
|
2886
|
+
networkStatePart.setState({
|
|
2887
|
+
...networkStatePart.getState()!,
|
|
2888
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
2889
|
+
});
|
|
2890
|
+
} catch (error) {
|
|
2891
|
+
console.error('IP intelligence refresh failed:', error);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
if (currentView === 'security') {
|
|
2896
|
+
try {
|
|
2897
|
+
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
console.error('Security policy refresh failed:', error);
|
|
2900
|
+
}
|
|
2668
2901
|
}
|
|
2669
2902
|
|
|
2670
2903
|
// Refresh certificate data if on Domains > Certificates subview
|
|
@@ -255,6 +255,17 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
255
255
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
.intelligenceBadge {
|
|
259
|
+
display: inline-flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
padding: 4px 8px;
|
|
262
|
+
border-radius: 999px;
|
|
263
|
+
font-size: 12px;
|
|
264
|
+
font-weight: 500;
|
|
265
|
+
background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
|
|
266
|
+
color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
|
|
267
|
+
}
|
|
268
|
+
|
|
258
269
|
.protocolChartGrid {
|
|
259
270
|
display: grid;
|
|
260
271
|
grid-template-columns: repeat(2, 1fr);
|
|
@@ -345,6 +356,100 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
345
356
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
346
357
|
}
|
|
347
358
|
|
|
359
|
+
private formatOptional(value: unknown): string {
|
|
360
|
+
if (value === null || value === undefined || value === '') return '-';
|
|
361
|
+
return String(value);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private formatDateTime(timestamp?: number | null): string {
|
|
365
|
+
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private getIpIntelligence(ip: string): interfaces.data.IIpIntelligenceRecord | undefined {
|
|
369
|
+
return this.networkState.ipIntelligence?.find((record) => record.ipAddress === ip);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private getIpOrganization(record?: interfaces.data.IIpIntelligenceRecord): string {
|
|
373
|
+
return record?.asnOrg || record?.registrantOrg || '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private getIpIntelligenceColumns(ip: string): Record<string, unknown> {
|
|
377
|
+
const record = this.getIpIntelligence(ip);
|
|
378
|
+
const organization = this.getIpOrganization(record);
|
|
379
|
+
return {
|
|
380
|
+
'Intelligence': record
|
|
381
|
+
? html`<span class="intelligenceBadge">${this.formatOptional(organization || record.countryCode || 'Known')}</span>`
|
|
382
|
+
: html`<span class="statusBadge warning">Enriching...</span>`,
|
|
383
|
+
'ASN': record?.asn ? `AS${record.asn}` : '-',
|
|
384
|
+
'Organization': this.formatOptional(organization),
|
|
385
|
+
'Country': this.formatOptional(record?.countryCode || record?.country),
|
|
386
|
+
'Network Range': this.formatOptional(record?.networkRange),
|
|
387
|
+
'Last Seen': this.formatDateTime(record?.lastSeenAt),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private getIpDataActions() {
|
|
392
|
+
return [
|
|
393
|
+
{
|
|
394
|
+
name: 'Refresh Intelligence',
|
|
395
|
+
iconName: 'lucide:refresh-cw',
|
|
396
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
397
|
+
actionFunc: async (actionData: any) => {
|
|
398
|
+
const ip = actionData.item.ip;
|
|
399
|
+
await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, ip);
|
|
400
|
+
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'Block IP',
|
|
405
|
+
iconName: 'lucide:shield-ban',
|
|
406
|
+
type: ['inRow', 'contextmenu'] as any,
|
|
407
|
+
actionFunc: async (actionData: any) => {
|
|
408
|
+
await this.createBlockRuleDialog('ip', actionData.item.ip, 'Blocked from Network Activity');
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'Block Network Range',
|
|
413
|
+
iconName: 'lucide:network',
|
|
414
|
+
type: ['contextmenu'] as any,
|
|
415
|
+
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.networkRange),
|
|
416
|
+
actionFunc: async (actionData: any) => {
|
|
417
|
+
const record = this.getIpIntelligence(actionData.item.ip);
|
|
418
|
+
await this.createBlockRuleDialog('cidr', record!.networkRange!, 'Blocked network range from Network Activity');
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'Block ASN',
|
|
423
|
+
iconName: 'lucide:radio-tower',
|
|
424
|
+
type: ['contextmenu'] as any,
|
|
425
|
+
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.asn),
|
|
426
|
+
actionFunc: async (actionData: any) => {
|
|
427
|
+
const record = this.getIpIntelligence(actionData.item.ip);
|
|
428
|
+
await this.createBlockRuleDialog('asn', String(record!.asn), 'Blocked ASN from Network Activity');
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'Block Organization',
|
|
433
|
+
iconName: 'lucide:building-2',
|
|
434
|
+
type: ['contextmenu'] as any,
|
|
435
|
+
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpOrganization(this.getIpIntelligence(actionData.item.ip))),
|
|
436
|
+
actionFunc: async (actionData: any) => {
|
|
437
|
+
const record = this.getIpIntelligence(actionData.item.ip);
|
|
438
|
+
await this.createBlockRuleDialog('organization', this.getIpOrganization(record), 'Blocked organization from Network Activity');
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'View Intelligence',
|
|
443
|
+
iconName: 'lucide:info',
|
|
444
|
+
type: ['doubleClick', 'contextmenu'] as any,
|
|
445
|
+
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)),
|
|
446
|
+
actionFunc: async (actionData: any) => {
|
|
447
|
+
await this.showIpIntelligenceDetails(actionData.item.ip);
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
|
|
348
453
|
private calculateThroughput(): { in: number; out: number } {
|
|
349
454
|
// Use real throughput data from network state
|
|
350
455
|
return {
|
|
@@ -500,10 +605,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
500
605
|
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
|
501
606
|
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
|
502
607
|
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
|
608
|
+
...this.getIpIntelligenceColumns(ipData.ip),
|
|
503
609
|
};
|
|
504
610
|
}}
|
|
611
|
+
.dataActions=${this.getIpDataActions()}
|
|
505
612
|
heading1="Top Connected IPs"
|
|
506
|
-
heading2="IPs with most active connections and
|
|
613
|
+
heading2="IPs with most active connections, bandwidth, and intelligence"
|
|
507
614
|
searchable
|
|
508
615
|
.showColumnFilters=${true}
|
|
509
616
|
.pagination=${false}
|
|
@@ -529,10 +636,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
529
636
|
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
|
530
637
|
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
|
531
638
|
'Connections': ipData.count,
|
|
639
|
+
...this.getIpIntelligenceColumns(ipData.ip),
|
|
532
640
|
};
|
|
533
641
|
}}
|
|
642
|
+
.dataActions=${this.getIpDataActions()}
|
|
534
643
|
heading1="Top IPs by Bandwidth"
|
|
535
|
-
heading2="IPs with highest throughput"
|
|
644
|
+
heading2="IPs with highest throughput and intelligence"
|
|
536
645
|
searchable
|
|
537
646
|
.showColumnFilters=${true}
|
|
538
647
|
.pagination=${false}
|
|
@@ -678,6 +787,114 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|
|
678
787
|
});
|
|
679
788
|
}
|
|
680
789
|
|
|
790
|
+
private getDropdownKey(value: any): string {
|
|
791
|
+
return typeof value === 'string' ? value : value?.key || '';
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private async createBlockRuleDialog(
|
|
795
|
+
type: interfaces.data.TSecurityBlockRuleType,
|
|
796
|
+
value: string,
|
|
797
|
+
reason: string,
|
|
798
|
+
): Promise<void> {
|
|
799
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
800
|
+
const typeOptions = [
|
|
801
|
+
{ key: 'ip', option: 'IP address' },
|
|
802
|
+
{ key: 'cidr', option: 'CIDR / network range' },
|
|
803
|
+
{ key: 'asn', option: 'ASN' },
|
|
804
|
+
{ key: 'organization', option: 'Organization' },
|
|
805
|
+
];
|
|
806
|
+
const matchModeOptions = [
|
|
807
|
+
{ key: 'contains', option: 'Organization contains value' },
|
|
808
|
+
{ key: 'exact', option: 'Organization exactly matches value' },
|
|
809
|
+
];
|
|
810
|
+
|
|
811
|
+
await DeesModal.createAndShow({
|
|
812
|
+
heading: 'Create Security Block Rule',
|
|
813
|
+
content: html`
|
|
814
|
+
<dees-form>
|
|
815
|
+
<dees-input-dropdown
|
|
816
|
+
.key=${'type'}
|
|
817
|
+
.label=${'Rule Type'}
|
|
818
|
+
.options=${typeOptions}
|
|
819
|
+
.selectedOption=${typeOptions.find((option) => option.key === type)}
|
|
820
|
+
></dees-input-dropdown>
|
|
821
|
+
<dees-input-text .key=${'value'} .label=${'Value'} .value=${value} .required=${true}></dees-input-text>
|
|
822
|
+
<dees-input-dropdown
|
|
823
|
+
.key=${'matchMode'}
|
|
824
|
+
.label=${'Organization Match Mode'}
|
|
825
|
+
.description=${'Only used for organization rules'}
|
|
826
|
+
.options=${matchModeOptions}
|
|
827
|
+
.selectedOption=${matchModeOptions[0]}
|
|
828
|
+
></dees-input-dropdown>
|
|
829
|
+
<dees-input-text .key=${'reason'} .label=${'Reason'} .value=${reason}></dees-input-text>
|
|
830
|
+
<dees-input-checkbox .key=${'enabled'} .label=${'Enable immediately'} .value=${true}></dees-input-checkbox>
|
|
831
|
+
</dees-form>
|
|
832
|
+
`,
|
|
833
|
+
menuOptions: [
|
|
834
|
+
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
|
835
|
+
{
|
|
836
|
+
name: 'Create',
|
|
837
|
+
iconName: 'lucide:shield-ban',
|
|
838
|
+
action: async (modalArg: any) => {
|
|
839
|
+
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
|
840
|
+
if (!form) return;
|
|
841
|
+
const data = await form.collectFormData();
|
|
842
|
+
const selectedType = this.getDropdownKey(data.type) as interfaces.data.TSecurityBlockRuleType;
|
|
843
|
+
const selectedValue = String(data.value || '').trim();
|
|
844
|
+
if (!selectedType || !selectedValue) return;
|
|
845
|
+
const matchMode = selectedType === 'organization'
|
|
846
|
+
? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
|
|
847
|
+
: undefined;
|
|
848
|
+
await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
|
|
849
|
+
type: selectedType,
|
|
850
|
+
value: selectedValue,
|
|
851
|
+
matchMode,
|
|
852
|
+
reason: String(data.reason || '').trim() || undefined,
|
|
853
|
+
enabled: data.enabled !== false,
|
|
854
|
+
});
|
|
855
|
+
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
|
856
|
+
await modalArg.destroy();
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private async showIpIntelligenceDetails(ip: string): Promise<void> {
|
|
864
|
+
const record = this.getIpIntelligence(ip);
|
|
865
|
+
if (!record) return;
|
|
866
|
+
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
867
|
+
|
|
868
|
+
await DeesModal.createAndShow({
|
|
869
|
+
heading: `IP Intelligence: ${ip}`,
|
|
870
|
+
content: html`
|
|
871
|
+
<div style="padding: 20px;">
|
|
872
|
+
<dees-dataview-codebox
|
|
873
|
+
.heading=${'Intelligence Record'}
|
|
874
|
+
progLang="json"
|
|
875
|
+
.codeToDisplay=${JSON.stringify(record, null, 2)}
|
|
876
|
+
></dees-dataview-codebox>
|
|
877
|
+
</div>
|
|
878
|
+
`,
|
|
879
|
+
menuOptions: [
|
|
880
|
+
{
|
|
881
|
+
name: 'Copy Abuse Contact',
|
|
882
|
+
iconName: 'lucide:copy',
|
|
883
|
+
action: async () => {
|
|
884
|
+
if (record.abuseContact) await navigator.clipboard.writeText(record.abuseContact);
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: 'Block IP',
|
|
889
|
+
iconName: 'lucide:shield-ban',
|
|
890
|
+
action: async () => {
|
|
891
|
+
await this.createBlockRuleDialog('ip', record.ipAddress, 'Blocked from IP intelligence details');
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
681
898
|
private async updateNetworkData() {
|
|
682
899
|
// Track requests/sec history for the trend sparkline (moved out of render)
|
|
683
900
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|