Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1c8a80e
feat(webui): add 5 security operations dashboards
r00tFe1 Jul 6, 2026
e1d3d85
feat(dream): add self-improvement evolution
Jul 30, 2026
e87fbc8
Merge branch 'dev' of github.com:AgentFlocks/flocks into feat/dream-s…
Aug 6, 2026
c7413e1
fix(memory): resolve configured embedding providers
Aug 6, 2026
8bc30ae
refactor(prompt): simplify agent instructions
Aug 6, 2026
5a726e0
feat(memory): bound injected memory snapshots
Aug 6, 2026
c6787cc
Merge remote-tracking branch 'origin/dev' into feat/dream-self-improv…
Aug 6, 2026
02b3f3f
feat(memory): refine search and curation rules
Aug 7, 2026
23afcce
fix(dream): enforce session read policy
Aug 7, 2026
7d9d0b2
Merge remote-tracking branch 'origin/dev' into feat/dream-self-improv…
Aug 7, 2026
21c4928
fix(dream): tighten durable memory curation
Aug 12, 2026
fbc2608
chore: sync main into dev after main-a01c66329631
github-actions[bot] Aug 12, 2026
b6e1638
Merge pull request #704 from AgentFlocks/chore/sync-main-into-dev-mai…
duguwanglong Aug 12, 2026
9d34dbd
Merge origin/dev into feat/dream-self-improvement
Aug 13, 2026
5851a48
feat: link SOC workflows to execution details
Aug 13, 2026
ac36a46
Merge pull request #706 from AgentFlocks/codex/workflow-execution-det…
stephamie7 Aug 13, 2026
564dfff
Merge pull request #522 from r00tFe1/feat/five-security-dashboards
stephamie7 Aug 13, 2026
b85164b
Revert "Merge pull request #522 from r00tFe1/feat/five-security-dashb…
stephamie7 Aug 13, 2026
2cbd50e
Merge pull request #707 from AgentFlocks/codex/revert-pr-522
stephamie7 Aug 13, 2026
5debe2f
fix(memory): harden dream execution
Aug 13, 2026
d76c6e3
Merge pull request #666 from AgentFlocks/feat/dream-self-improvement
duguwanglong Aug 13, 2026
9d6858e
fix provider cache usage accounting
Aug 13, 2026
54ffaac
Merge pull request #708 from AgentFlocks/codex/fix-provider-cache-usage
stephamie7 Aug 13, 2026
b1265bc
fix(kafka): align concurrency limits with syslog
Aug 14, 2026
4fc9c62
feat(edr): add advanced threat API collection
luguili-booker Aug 14, 2026
1cc71c4
Merge pull request #710 from AgentFlocks/codex/edr-advanced-threat-api
stephamie7 Aug 14, 2026
6a06438
fix(channel): persist cleared WeCom WebSocket URL
stephamie7 Aug 14, 2026
4bede46
Merge pull request #711 from AgentFlocks/fix/wecom-websocket-url-clear
duguwanglong Aug 14, 2026
58f65bf
fix(session): match completed replies by parent
Aug 17, 2026
e425cb5
Merge pull request #712 from AgentFlocks/fix/session-loop-parent-exit
stephamie7 Aug 17, 2026
722317c
fix(session): prevent stale channel replies
duguwanglong Aug 17, 2026
f0aabb0
Merge pull request #713 from AgentFlocks/fix/wecom-session-stale-resp…
stephamie7 Aug 17, 2026
04ece7a
fix(tool-search): restore session callable tool registration
duguwanglong Aug 17, 2026
a8319e3
Merge pull request #714 from AgentFlocks/fix/restore-session-callable…
stephamie7 Aug 17, 2026
25c52ac
test: prune obsolete and redundant test coverage
duguwanglong Aug 17, 2026
a631efe
Merge pull request #715 from AgentFlocks/feat/prune-obsolete-test-scr…
xiami762 Aug 17, 2026
234908c
Merge pull request #709 from AgentFlocks/fix/kafka-concurrency
duguwanglong Aug 17, 2026
64518bd
chore/update-version-2026-8-17
stephamie7 Aug 17, 2026
4929cd3
Merge pull request #717 from AgentFlocks/chore/update-version-2026.8.17
duguwanglong Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 80 additions & 44 deletions .flocks/flockshub/plugins/webuis/soc_ui/soc_dashboard/src/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,17 @@ function createActivityState() {

function createMockActivityEvent(overrides) {
const now = new Date();
const workflowId = overrides.workflowId || (overrides.stage === 'triage' ? 'stream_alert_triage' : 'stream_alert_denoise');
const executionId = overrides.executionId || `${workflowId}-mock-execution`;
return {
eventId: overrides.eventId || `mock-${overrides.stage}-${overrides.alert?.id || Math.random().toString(36).slice(2)}`,
eventId: overrides.eventId || `workflow-execution:${executionId}`,
stage: overrides.stage,
status: overrides.status || 'running',
occurredAt: overrides.occurredAt || now.toISOString(),
triggerSource: overrides.triggerSource || 'mock',
triggerSource: overrides.triggerSource || 'workflow_execution',
workflowId,
sessionId: overrides.sessionId || '',
messageId: overrides.messageId || '',
playbackMode: overrides.playbackMode || 'normal',
playbackStartedAt: overrides.playbackStartedAt || Date.now() - 4200,
sampleCount: overrides.sampleCount || 1,
Expand Down Expand Up @@ -234,7 +239,8 @@ function createMockActivityState() {
const now = Date.now();
const denoiseCurrent = createMockActivityEvent({
stage: 'denoise',
eventId: 'mock-denoise-current',
workflowId: 'stream_alert_denoise',
executionId: 'mock-denoise-run-001',
playbackMode: 'burst',
playbackStartedAt: now - 3600,
sampleCount: 6,
Expand All @@ -259,7 +265,8 @@ function createMockActivityState() {
});
const triageCurrent = createMockActivityEvent({
stage: 'triage',
eventId: 'mock-triage-current',
workflowId: 'stream_alert_triage',
executionId: 'mock-triage-run-002',
playbackStartedAt: now - 6200,
alert: {
id: 'mock-alert-rce',
Expand All @@ -280,7 +287,8 @@ function createMockActivityState() {
const triageWaiting = createMockActivityEvent({
stage: 'triage',
status: 'queued',
eventId: 'mock-triage-waiting',
workflowId: 'stream_alert_triage',
executionId: 'mock-triage-run-003',
occurredAt: new Date(now - 18000).toISOString(),
playbackStartedAt: now - 18000,
alert: {
Expand All @@ -300,7 +308,8 @@ function createMockActivityState() {
const denoiseWaiting = createMockActivityEvent({
stage: 'denoise',
status: 'queued',
eventId: 'mock-denoise-waiting',
workflowId: 'stream_alert_denoise',
executionId: 'mock-denoise-run-004',
occurredAt: new Date(now - 26000).toISOString(),
playbackStartedAt: now - 26000,
sampleCount: 4,
Expand Down Expand Up @@ -440,7 +449,7 @@ function createMockTaskCenterState() {
latestAlertName: '异常登录爆发(Mock)',
progressPercent: 0.58,
progressLabel: '第 4/7 步',
currentPhase: '聚类降噪',
currentPhase: 'running',
sessionId: '',
messageId: '',
},
Expand All @@ -458,7 +467,7 @@ function createMockTaskCenterState() {
latestAlertName: '远程命令执行攻击(Mock)',
progressPercent: 0.67,
progressLabel: '第 2/3 步',
currentPhase: '证据汇总',
currentPhase: 'running',
sessionId: '',
messageId: '',
},
Expand Down Expand Up @@ -2177,26 +2186,53 @@ function taskCenterProgressLabel(item) {
return String(item?.progressLabel || '').trim() || '待执行';
}

function openTaskCenterConversation(item) {
const sessionId = String(item?.sessionId || item?.sessionID || '').trim();
if (!sessionId || typeof window === 'undefined') return false;
const messageId = String(item?.messageId || item?.messageID || '').trim();
const params = new URLSearchParams({ session: sessionId });
if (messageId) params.set('focusMessage', messageId);
window.location.href = `/sessions?${params.toString()}`;
return true;
function workflowIdFromTaskCenterItem(item) {
return String(item?.id || item?.workflowId || item?.workflowID || '').trim();
}

function executionIdFromTaskCenterItem(item) {
return String(item?.latestExecutionHash || item?.executionId || item?.executionID || '').trim();
}

function executionIdFromWorkflowEvent(event) {
const directId = String(event?.executionId || event?.executionID || '').trim();
if (directId) return directId;
const eventId = String(event?.eventId || event?.id || '').trim();
const match = eventId.match(/^workflow-execution:(.+)$/);
return match ? match[1].trim() : '';
}

function workflowIdFromEvent(event) {
return String(event?.workflowId || event?.workflowID || '').trim();
}

function openConversationFromEvent(event) {
const sessionId = String(event?.sessionId || event?.sessionID || '').trim();
if (!sessionId || typeof window === 'undefined') return false;
const messageId = String(event?.messageId || event?.messageID || '').trim();
const params = new URLSearchParams({ session: sessionId });
if (messageId) params.set('focusMessage', messageId);
window.location.href = `/sessions?${params.toString()}`;
function openWorkflowExecution(workflowId, executionId) {
if (!workflowId || !executionId || typeof window === 'undefined') return false;
const params = new URLSearchParams({ tab: 'run', execId: executionId });
try {
const currentParams = new URLSearchParams(window.location.search || '');
if (currentParams.has('mockDashboard') || currentParams.has('mockActivity') || currentParams.has('mockTaskCenter')) {
params.set('mockDashboard', '1');
} else if (isMockSwitchEnabled(window.localStorage?.getItem(SOC_MOCK_DASHBOARD_KEY))
|| isMockSwitchEnabled(window.localStorage?.getItem(SOC_MOCK_ACTIVITY_KEY))
|| isMockSwitchEnabled(window.localStorage?.getItem(SOC_MOCK_TASK_CENTER_KEY))) {
params.set('mockDashboard', '1');
}
} catch {
// Mock switch propagation is best-effort; navigation must still work without localStorage.
}
window.location.href = `/workflows/${encodeURIComponent(workflowId)}?${params.toString()}`;
return true;
}

function openWorkflowExecutionFromTaskCenter(item) {
return openWorkflowExecution(workflowIdFromTaskCenterItem(item), executionIdFromTaskCenterItem(item));
}

function openWorkflowExecutionFromEvent(event) {
return openWorkflowExecution(workflowIdFromEvent(event), executionIdFromWorkflowEvent(event));
}

function TaskCenterSummary({ taskCenter }) {
const scheduledTasks = taskCenter.scheduledTasks || [];
const workflows = taskCenter.workflows || [];
Expand Down Expand Up @@ -2234,7 +2270,7 @@ function TaskCenterItem({ item, kind }) {
const latestExecutionHash = taskCenterHashValue(item.latestExecutionHash);
const itemName = kind === 'workflow' ? taskCenterWorkflowName(item) : item.name || item.id;
const alertName = String(item.latestAlertName || '').trim();
const hasConversation = kind === 'workflow' && Boolean(String(item.sessionId || item.sessionID || '').trim());
const hasExecution = kind === 'workflow' && Boolean(workflowIdFromTaskCenterItem(item) && executionIdFromTaskCenterItem(item));
const scheduledClosed = kind === 'scheduled' && ['disabled', 'stopped'].includes(schedulerStatus);
const sub = kind === 'scheduled'
? scheduledClosed
Expand All @@ -2259,20 +2295,20 @@ function TaskCenterItem({ item, kind }) {
h('span', { key: 'rate' }, ['成功率 ', h('b', { key: 'value' }, taskCenterPercent(successRate))]),
];
const handleOpen = () => {
if (hasConversation) openTaskCenterConversation(item);
if (hasExecution) openWorkflowExecutionFromTaskCenter(item);
};
const handleKeyDown = (event) => {
if (!hasConversation) return;
if (!hasExecution) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openTaskCenterConversation(item);
openWorkflowExecutionFromTaskCenter(item);
}
};
return h('article', {
className: cx('task-center-item', active && 'active', hasConversation && 'clickable'),
role: hasConversation ? 'button' : undefined,
tabIndex: hasConversation ? 0 : undefined,
title: hasConversation ? '打开对应对话' : undefined,
className: cx('task-center-item', active && 'active', hasExecution && 'clickable'),
role: hasExecution ? 'button' : undefined,
tabIndex: hasExecution ? 0 : undefined,
title: hasExecution ? '打开执行详情' : undefined,
onClick: handleOpen,
onKeyDown: handleKeyDown,
}, [
Expand All @@ -2292,8 +2328,8 @@ function TaskCenterItem({ item, kind }) {
kind === 'workflow' ? h('div', { className: 'task-center-hash', title: latestExecutionHash, key: 'hash' }, [
h('span', { key: 'label' }, '执行ID'),
h('code', { key: 'value' }, latestExecutionHash),
h('span', { key: 'link-label' }, '关联对话'),
h('code', { className: cx('task-center-jump', hasConversation && 'enabled'), key: 'link' }, hasConversation ? '查看对话' : '暂无关联对话'),
h('span', { key: 'link-label' }, '执行详情'),
h('code', { className: cx('task-center-jump', hasExecution && 'enabled'), key: 'link' }, hasExecution ? '查看执行' : '暂无执行记录'),
]) : null,
h('div', { className: cx('task-center-stats', kind === 'workflow' && 'workflow-stats'), key: 'stats' }, stats),
h('div', {
Expand Down Expand Up @@ -2414,23 +2450,23 @@ function CommandAiTaskPanel({ activity, timeFilter }) {
: task.state === 'processing'
? task.stage === 'triage' ? '证据关联与结论生成中' : '特征提取与相似聚类中'
: '等待 AI 处理';
const hasConversation = Boolean(String(event?.sessionId || event?.sessionID || '').trim());
const hasExecution = Boolean(workflowIdFromEvent(event) && executionIdFromWorkflowEvent(event));
const handleOpen = () => {
if (hasConversation) openConversationFromEvent(event);
if (hasExecution) openWorkflowExecutionFromEvent(event);
};
const handleKeyDown = (keyboardEvent) => {
if (!hasConversation) return;
if (!hasExecution) return;
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
keyboardEvent.preventDefault();
openConversationFromEvent(event);
openWorkflowExecutionFromEvent(event);
}
};
return h('article', {
className: cx('event-rail-item', `state-${task.state}`, `kind-${task.stage}`, `motion-${task.motion || 'stable'}`, hasConversation && 'clickable'),
className: cx('event-rail-item', `state-${task.state}`, `kind-${task.stage}`, `motion-${task.motion || 'stable'}`, hasExecution && 'clickable'),
key: task.key,
role: hasConversation ? 'button' : undefined,
tabIndex: hasConversation ? 0 : undefined,
title: hasConversation ? '打开对应对话' : undefined,
role: hasExecution ? 'button' : undefined,
tabIndex: hasExecution ? 0 : undefined,
title: hasExecution ? '打开执行详情' : undefined,
onClick: handleOpen,
onKeyDown: handleKeyDown,
}, [
Expand All @@ -2441,7 +2477,7 @@ function CommandAiTaskPanel({ activity, timeFilter }) {
]),
h('strong', { title, key: 'title' }, title),
h('span', { title: eventEndpoint(event), key: 'endpoint' }, eventEndpoint(event)),
h('small', { key: 'result' }, hasConversation ? `${detail} · 查看对话` : detail),
h('small', { key: 'result' }, hasExecution ? `${detail} · 查看执行` : detail),
task.state === 'processing' ? h(EventQueueProgress, { event, key: 'progress' }) : null,
]);
}) : h('div', { className: 'event-rail-empty' }, '等待新的降噪或研判任务')),
Expand Down Expand Up @@ -2752,8 +2788,8 @@ export default function Page() {
const incomingEvents = rawIncomingEvents.filter((event) => event?.stage !== 'denoise');
const workflowEvents = Array.isArray(payload.workflowEvents) ? payload.workflowEvents : [];
for (const workflowEvent of workflowEvents) {
const hasConversation = Boolean(String(workflowEvent?.sessionId || workflowEvent?.sessionID || '').trim());
if (hasConversation) {
const hasExecution = Boolean(workflowIdFromEvent(workflowEvent) && executionIdFromWorkflowEvent(workflowEvent));
if (hasExecution) {
incomingEvents.push(workflowEvent);
}
}
Expand Down
33 changes: 32 additions & 1 deletion .flocks/plugins/skills/sangfor-edr-use/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: sangfor-edr-use
description: 深信服 EDR 登录态管理、首页仪表盘、威胁资产分析和资产清点分类统计 API 采集。用户提到深信服 EDR、EDR、资产清点或 sangfor EDR 时必须先加载本 skill。
description: 深信服 EDR 登录态管理、首页仪表盘、威胁资产分析、资产清点和高级威胁 API 采集。用户提到深信服 EDR、EDR、资产清点、高级威胁或 sangfor EDR 时必须先加载本 skill。
---

# 深信服 EDR Use
Expand All @@ -15,6 +15,8 @@ description: 深信服 EDR 登录态管理、首页仪表盘、威胁资产分
HTTP 登录模块验证过的同一套 Cookie/token。
`sangfor_edr_asset_inventory_api.py` 负责资产清点页面 API 请求,同样只读取
HTTP 登录模块验证过的同一套 Cookie/token。
`sangfor_edr_advanced_threat_api.py` 负责高级威胁告警模式和事件模式 API 请求,
同样只读取 HTTP 登录模块验证过的同一套 Cookie/token。

- 管理同一次登录产生的 Cookie 与 `login_token`。
- 默认使用 HTTP 登录,开始前必须向用户索取并保存 EDR 地址、用户名和密码。
Expand All @@ -24,6 +26,7 @@ HTTP 登录模块验证过的同一套 Cookie/token。
- 通过 API 采集首页终端概况、受影响终端、漏洞、勒索防护、实时病毒、Top 5 终端和设备资源使用率。
- 通过 API 采集威胁资产分析的风险汇总、资产分组和威胁终端事件列表,支持风险级别、资产分组、终端状态、隔离状态和分页筛选。
- 通过 API 采集资产清点页面的资产分类统计。
- 通过 API 采集高级威胁的告警模式和事件模式列表,支持多选筛选、时间范围和自动分页。

## 输入与输出

Expand Down Expand Up @@ -73,6 +76,20 @@ HTTP 登录模块验证过的同一套 Cookie/token。

分类字段映射由采集模块维护;`Replace` 按页面显示映射为“真替真用”,英文保留原始字段名 `Replace`。

### 高级威胁工具

调用 `sangfor_edr_advanced_threat`,可输入:

- `sections`:`warning_logs`、`incidents`;省略时采集两种模式。
- `threat_levels`:威胁等级单值或多值。
- `disposal_states`、`event_disposal_states`、`agent_types`、`detect_sources`、`event_types`:事件模式多选条件。
- `wl_switch`:已例外告警单选;`0`=隐藏,`1`=显示。
- `days`:默认最近 7 天;或成对提供 `begin_time`、`end_time`。
- `page_no`、`page_limit`、`paginate`:分页配置,默认自动采集全部。
- `base_url`、`auth_state_path`:可选运行时覆盖。

输出包含 `data`/`raw_data`、`readable_data`、分页元数据和分项 `errors`;不得输出 Cookie、密码或 `login_token`。

## 关键配置

- `base_url`:从用户提供的 EDR 地址提取 scheme、host 和 port;不得使用固定示例地址。
Expand Down Expand Up @@ -102,6 +119,18 @@ HTTP 登录模块验证过的同一套 Cookie/token。
- `limit`:只能使用 `10/20/50/100/500`。
- `zone_name`:先调用 `list_zones`,按返回的 `zone_name` 或 `full_zone_name` 精确匹配,再将对应的设备专属 `zone_id` 放入 `list_agent_event.filter.zone_id`;不能使用固定 zone ID,也不能把中文分组名直接作为 `zone_id`。
8. 资产清点 API 使用 `POST /api/edrgoweb/v1/asset/inventory/classify?s={login_token}`,payload 为 `{"sceneType":"server_and_pc"}`;复用当前登录会话的 Cookie 和同一 `login_token`,接口返回 `code != 0` 时视为失败。
9. 高级威胁 API 使用:
- `POST /api/edrgoweb/v1/advthreats/querywarninglogs?_method=get&s={login_token}`:告警模式列表,只发送已确认的 `threatLevel`、`wlSwitch` 筛选。
- `POST /api/edrgoweb/v1/advthreats/queryincidentinfo?_method=get&s={login_token}`:事件模式列表,发送威胁等级、处置状态、实时防护、终端类型、检测来源、事件标签、时间范围和已例外告警筛选。
- `uuid` 是动态请求关联标识,不是设备 ID;`uid` 默认使用当前登录用户名,`tid` 默认 `0`。
10. 高级威胁筛选映射:
- `threatLevel`:`5`=严重、`4`=高危、`3`=中危、`2`=低危、`1`=信息。
- `disposalState`:`0`=待处置、`2`=已处置、`3`=已忽略。
- `eventDisposalState`:`0`=暂不支持、`1`=未处置、`2`=自动处置中、`3`=已自动处置。
- `agentType`:`0`=PC、`1`=服务器。
- `detectSource`:`1`=IOC引擎、`2`=IOA引擎、`3`=SIP联动、`6`=MS引擎、`8`=AF联动;响应中的未知值保留原值并标记“未知”。
- `eventType`:`1`=钓鱼攻击、`2`=Web入侵、`3`=恶意病毒、`0`=其他。
- 除时间范围和 `wlSwitch` 外,上述业务筛选均支持多选。

## 错误处理

Expand All @@ -111,6 +140,7 @@ HTTP 登录模块验证过的同一套 Cookie/token。
- 认证探测失败:禁止继续业务 API;先执行 HTTP 重登并再次探测,连续 3 次 HTTP 仍失败则按 browser/CDP 自动化登录→手动登录降级。
- 仪表盘部分接口失败:保留成功数据,在 `errors` 中按采集项返回失败原因。
- 资产清点接口失败:在 `errors` 中返回接口失败原因,不输出敏感认证信息。
- 高级威胁单个模式失败:保留另一个模式的成功数据,并在 `errors` 中按模式返回脱敏原因。
- Cookie、密码和 `login_token` 不得回显、记录日志或混入业务输出。

## 执行约束
Expand All @@ -124,4 +154,5 @@ HTTP 登录模块验证过的同一套 Cookie/token。
- 任何 API 采集前必须完成认证探测;认证探测失败时不得继续调用业务接口。
- 威胁资产分析的分页请求必须复用同一套 Cookie/token,不得在分页过程中重新拼接或替换认证参数。
- 资产清点 API 请求必须复用同一套 Cookie/token;不得把抓包中的 sessionid、token 或设备地址写死为通用凭据或地址。
- 高级威胁分页必须复用同一套 Cookie/token;不得输出响应 `req` 中的 token,也不得把抓包中的 `uid`、token、Cookie 或设备地址写死。
- 用户未提供接口参数名时,必须根据中文语义完成上述映射;无法确认的筛选条件不得猜测数值,应省略筛选或向用户确认。
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ version: "1.0.0"
integration_type: device
description: >
Sangfor EDR integration with default HTTP login, explicitly selected
browser/CDP login, cookie/token validation, dashboard, threat-asset, and asset-inventory API collection.
browser/CDP login, cookie/token validation, dashboard, threat-asset,
asset-inventory, and advanced-threat API collection.
description_cn: >
深信服 EDR 集成。默认通过 HTTP 登录,仅用户明确选择时使用 browser/CDP;
每次操作验证成套 Cookie/token,并通过 API 采集首页仪表盘、威胁资产分析和资产清点
每次操作验证成套 Cookie/token,并通过 API 采集首页仪表盘、威胁资产分析、资产清点和高级威胁
credential_fields:
- key: base_url
label: Base URL
Expand Down Expand Up @@ -113,7 +114,8 @@ defaults:
verify_ssl: false
notes: |
All login methods save cookies to auth-state.json and login_token to Secret
Manager. Pairing metadata prevents dashboard, threat-asset, and asset-inventory APIs from mixing credentials
Manager. Pairing metadata prevents dashboard, threat-asset, asset-inventory,
and advanced-threat APIs from mixing credentials
produced by different logins. Device URLs are normalized to scheme, host,
and port before use.

Expand Down
Loading
Loading