diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index e4682cb..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,89 +0,0 @@ -# Copilot instructions for ActivityWatch-Russian - -Purpose: help future Copilot sessions quickly understand how to build, validate, and modify this repo. - ---- - -## Build / test / lint (how-to) - -- Shell script checks (CI & local): - - Full: ./scripts/quality-gate.sh - - Single file (syntax): bash -n + + + +
+ + + + + \ No newline at end of file diff --git a/graphify-out/graph.json b/graphify-out/graph.json new file mode 100644 index 0000000..eb91b0d --- /dev/null +++ b/graphify-out/graph.json @@ -0,0 +1,11645 @@ +{ + "nodes": [ + { + "id": "aw_server_aw_ru_patch_js", + "label": "aw-ru-patch.js", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1", + "community": 2 + }, + { + "id": "aw_ru_patch_replacetext", + "label": "replaceText()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L269", + "community": 8 + }, + { + "id": "aw_ru_patch_walk", + "label": "walk()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L281", + "community": 8 + }, + { + "id": "aw_ru_patch_translateattributes", + "label": "translateAttributes()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L293", + "community": 8 + }, + { + "id": "aw_ru_patch_injectstyles", + "label": "injectStyles()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L305", + "community": 2 + }, + { + "id": "aw_ru_patch_hidenoisenavigation", + "label": "hideNoiseNavigation()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L348", + "community": 2 + }, + { + "id": "aw_ru_patch_getcurrenthostfromhash", + "label": "getCurrentHostFromHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L360", + "community": 2 + }, + { + "id": "aw_ru_patch_ispvelikehost", + "label": "isPveLikeHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L369", + "community": 2 + }, + { + "id": "aw_ru_patch_islikelyclienthost", + "label": "isLikelyClientHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L373", + "community": 2 + }, + { + "id": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "label": "enforceSafeActivityViewForPveHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L383", + "community": 2 + }, + { + "id": "aw_ru_patch_getdlphostfromsettings", + "label": "getDlpHostFromSettings()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L397", + "community": 2 + }, + { + "id": "aw_ru_patch_getdlphref", + "label": "getDlpHref()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L405", + "community": 2 + }, + { + "id": "aw_ru_patch_isdlpsignalbucketroute", + "label": "isDlpSignalBucketRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L410", + "community": 5 + }, + { + "id": "aw_ru_patch_isalertsroute", + "label": "isAlertsRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L414", + "community": 13 + }, + { + "id": "aw_ru_patch_getdlpbucketidfromhash", + "label": "getDlpBucketIdFromHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L418", + "community": 9 + }, + { + "id": "aw_ru_patch_getdlphostfrombucketid", + "label": "getDlpHostFromBucketId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L424", + "community": 5 + }, + { + "id": "aw_ru_patch_escapehtml", + "label": "escapeHtml()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L429", + "community": 13 + }, + { + "id": "aw_ru_patch_normalizetext", + "label": "normalizeText()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L438", + "community": 14 + }, + { + "id": "aw_ru_patch_builddlpkey", + "label": "buildDlpKey()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L442", + "community": 5 + }, + { + "id": "aw_ru_patch_generatedlpid", + "label": "generateDlpId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L454", + "community": 9 + }, + { + "id": "aw_ru_patch_awapijson", + "label": "awApiJson()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L458", + "community": 9 + }, + { + "id": "aw_ru_patch_ensureawbucket", + "label": "ensureAwBucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L476", + "community": 9 + }, + { + "id": "aw_ru_patch_saveawheartbeat", + "label": "saveAwHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L487", + "community": 9 + }, + { + "id": "aw_ru_patch_loadbucketevents", + "label": "loadBucketEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L495", + "community": 5 + }, + { + "id": "aw_ru_patch_getrulematchfields", + "label": "getRuleMatchFields()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L503", + "community": 14 + }, + { + "id": "aw_ru_patch_serializerulematch", + "label": "serializeRuleMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L515", + "community": 5 + }, + { + "id": "aw_ru_patch_getruleid", + "label": "getRuleId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L527", + "community": 5 + }, + { + "id": "aw_ru_patch_getreviewid", + "label": "getReviewId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L536", + "community": 5 + }, + { + "id": "aw_ru_patch_collapseruleevents", + "label": "collapseRuleEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L547", + "community": 5 + }, + { + "id": "aw_ru_patch_collapsereviewevents", + "label": "collapseReviewEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L560", + "community": 5 + }, + { + "id": "aw_ru_patch_rulematchesevent", + "label": "ruleMatchesEvent()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L573", + "community": 14 + }, + { + "id": "aw_ru_patch_getsuppressionstate", + "label": "getSuppressionState()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L582", + "community": 5 + }, + { + "id": "aw_ru_patch_removebaddlplinks", + "label": "removeBadDlpLinks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L595", + "community": 2 + }, + { + "id": "aw_ru_patch_updatedlplinks", + "label": "updateDlpLinks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L603", + "community": 2 + }, + { + "id": "aw_ru_patch_builddlpnavitem", + "label": "buildDlpNavItem()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L612", + "community": 2 + }, + { + "id": "aw_ru_patch_findprimarynavlist", + "label": "findPrimaryNavList()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L630", + "community": 2 + }, + { + "id": "aw_ru_patch_injectdlpnavigation", + "label": "injectDlpNavigation()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L643", + "community": 2 + }, + { + "id": "aw_ru_patch_ishomeroute", + "label": "isHomeRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L675", + "community": 8 + }, + { + "id": "aw_ru_patch_getdefaulthostgroupsconfig", + "label": "getDefaultHostGroupsConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L680", + "community": 8 + }, + { + "id": "aw_ru_patch_gethostgroupsstate", + "label": "getHostGroupsState()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L722", + "community": 8 + }, + { + "id": "aw_ru_patch_ensurehostgroupsdata", + "label": "ensureHostGroupsData()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L733", + "community": 8 + }, + { + "id": "aw_ru_patch_ispveactivityroute", + "label": "isPveActivityRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L755", + "community": 2 + }, + { + "id": "aw_ru_patch_extracthostfrombucket", + "label": "extractHostFromBucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L761", + "community": 8 + }, + { + "id": "aw_ru_patch_buildhostbucketmap", + "label": "buildHostBucketMap()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L788", + "community": 8 + }, + { + "id": "aw_ru_patch_hosthasbucketprefix", + "label": "hostHasBucketPrefix()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L804", + "community": 8 + }, + { + "id": "aw_ru_patch_matchhostgroup", + "label": "matchHostGroup()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L810", + "community": 8 + }, + { + "id": "aw_ru_patch_buildhostlink", + "label": "buildHostLink()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L839", + "community": 8 + }, + { + "id": "aw_ru_patch_renderhostgroupcards", + "label": "renderHostGroupCards()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L855", + "community": 8 + }, + { + "id": "aw_ru_patch_injecthostgroupscenter", + "label": "injectHostGroupsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L911", + "community": 8 + }, + { + "id": "aw_ru_patch_renderdlptablerows", + "label": "renderDlpTableRows()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L932", + "community": 5 + }, + { + "id": "aw_ru_patch_savedlpreview", + "label": "saveDlpReview()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L973", + "community": 9 + }, + { + "id": "aw_ru_patch_savedlpincident", + "label": "saveDlpIncident()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1007", + "community": 9 + }, + { + "id": "aw_ru_patch_savedlprule", + "label": "saveDlpRule()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1031", + "community": 9 + }, + { + "id": "aw_ru_patch_binddlprowactions", + "label": "bindDlpRowActions()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1052", + "community": 5 + }, + { + "id": "aw_ru_patch_setdlpruleenabled", + "label": "setDlpRuleEnabled()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1087", + "community": 5 + }, + { + "id": "aw_ru_patch_setdlpreviewarchived", + "label": "setDlpReviewArchived()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1106", + "community": 9 + }, + { + "id": "aw_ru_patch_renderdlprulemanager", + "label": "renderDlpRuleManager()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1129", + "community": 5 + }, + { + "id": "aw_ru_patch_renderdlpreviewmanager", + "label": "renderDlpReviewManager()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1174", + "community": 5 + }, + { + "id": "aw_ru_patch_refreshdlpcenter", + "label": "refreshDlpCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1220", + "community": 5 + }, + { + "id": "aw_ru_patch_injectdlpreviewcenter", + "label": "injectDlpReviewCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1249", + "community": 5 + }, + { + "id": "aw_ru_patch_refreshdlpalertscenter", + "label": "refreshDlpAlertsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1318", + "community": 13 + }, + { + "id": "aw_ru_patch_refreshpveauditcenter", + "label": "refreshPveAuditCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1348", + "community": 2 + }, + { + "id": "aw_ru_patch_injectpveauditcenter", + "label": "injectPveAuditCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1394", + "community": 2 + }, + { + "id": "aw_ru_patch_injectdlpalertscenter", + "label": "injectDlpAlertsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1432", + "community": 13 + }, + { + "id": "aw_ru_patch_gettrendshostfromsettings", + "label": "getTrendsHostFromSettings()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1475", + "community": 2 + }, + { + "id": "aw_ru_patch_gettrendspath", + "label": "getTrendsPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1483", + "community": 2 + }, + { + "id": "aw_ru_patch_shouldredirecttrends", + "label": "shouldRedirectTrends()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1489", + "community": 2 + }, + { + "id": "aw_ru_patch_redirectbaretrendsroute", + "label": "redirectBareTrendsRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1494", + "community": 2 + }, + { + "id": "aw_ru_patch_ensuresettingshost", + "label": "ensureSettingsHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1517", + "community": 2 + }, + { + "id": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "label": "getPreferredWindowHostFromBuckets()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1535", + "community": 2 + }, + { + "id": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "label": "rewriteUnknownCategoryBuilderQueryBody()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1552", + "community": 2 + }, + { + "id": "aw_ru_patch_installcategorybuildernetworkpatch", + "label": "installCategoryBuilderNetworkPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1578", + "community": 2 + }, + { + "id": "aw_ru_patch_patchcategorybuilderhostlabel", + "label": "patchCategoryBuilderHostLabel()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1625", + "community": 2 + }, + { + "id": "aw_ru_patch_patchactivityheading", + "label": "patchActivityHeading()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1642", + "community": 2 + }, + { + "id": "aw_ru_patch_applypatch", + "label": "applyPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1654", + "community": 2 + }, + { + "id": "aw_ru_patch_scheduleapplypatch", + "label": "scheduleApplyPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1673", + "community": 2 + }, + { + "id": "aw_server_aw_sw_cleanup_js", + "label": "aw-sw-cleanup.js", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-sw-cleanup.js", + "source_location": "L1", + "community": 15 + }, + { + "id": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "label": "aw_activitywatch.py", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L1", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter", + "label": "ActivityWatchExporter", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L31", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter_init", + "label": ".__init__()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L32", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter_get_buckets", + "label": ".get_buckets()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L38", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter_get_bucket_events", + "label": ".get_bucket_events()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L48", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter_get_bucket_info", + "label": ".get_bucket_info()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L62", + "community": 6 + }, + { + "id": "aw_activitywatch_activitywatchexporter_collect_metrics", + "label": ".collect_metrics()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L72", + "community": 6 + }, + { + "id": "aw_activitywatch_main", + "label": "main()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L125", + "community": 6 + }, + { + "id": "aw_activitywatch_rationale_39", + "label": "Get all buckets from ActivityWatch API.", + "file_type": "rationale", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L39", + "community": 6 + }, + { + "id": "aw_activitywatch_rationale_49", + "label": "Get events from a specific bucket.", + "file_type": "rationale", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L49", + "community": 6 + }, + { + "id": "aw_activitywatch_rationale_63", + "label": "Get detailed info about a bucket.", + "file_type": "rationale", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L63", + "community": 6 + }, + { + "id": "aw_activitywatch_rationale_73", + "label": "Collect metrics from ActivityWatch.", + "file_type": "rationale", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L73", + "community": 6 + }, + { + "id": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "label": "aw-ru-patch.js", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1", + "community": 8 + }, + { + "id": "aw_ru_patch_replacetext", + "label": "replaceText()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L269", + "community": 8 + }, + { + "id": "aw_ru_patch_walk", + "label": "walk()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L281", + "community": 8 + }, + { + "id": "aw_ru_patch_translateattributes", + "label": "translateAttributes()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L293", + "community": 8 + }, + { + "id": "aw_ru_patch_injectstyles", + "label": "injectStyles()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L305", + "community": 2 + }, + { + "id": "aw_ru_patch_hidenoisenavigation", + "label": "hideNoiseNavigation()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L348", + "community": 2 + }, + { + "id": "aw_ru_patch_getcurrenthostfromhash", + "label": "getCurrentHostFromHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L360", + "community": 2 + }, + { + "id": "aw_ru_patch_ispvelikehost", + "label": "isPveLikeHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L369", + "community": 2 + }, + { + "id": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "label": "enforceSafeActivityViewForPveHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L373", + "community": 2 + }, + { + "id": "aw_ru_patch_getdlphostfromsettings", + "label": "getDlpHostFromSettings()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L387", + "community": 2 + }, + { + "id": "aw_ru_patch_getdlphref", + "label": "getDlpHref()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L395", + "community": 2 + }, + { + "id": "aw_ru_patch_isdlpsignalbucketroute", + "label": "isDlpSignalBucketRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L400", + "community": 5 + }, + { + "id": "aw_ru_patch_isalertsroute", + "label": "isAlertsRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L404", + "community": 13 + }, + { + "id": "aw_ru_patch_getdlpbucketidfromhash", + "label": "getDlpBucketIdFromHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L408", + "community": 9 + }, + { + "id": "aw_ru_patch_getdlphostfrombucketid", + "label": "getDlpHostFromBucketId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L414", + "community": 5 + }, + { + "id": "aw_ru_patch_escapehtml", + "label": "escapeHtml()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L419", + "community": 13 + }, + { + "id": "aw_ru_patch_normalizetext", + "label": "normalizeText()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L428", + "community": 14 + }, + { + "id": "aw_ru_patch_builddlpkey", + "label": "buildDlpKey()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L432", + "community": 5 + }, + { + "id": "aw_ru_patch_generatedlpid", + "label": "generateDlpId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L444", + "community": 9 + }, + { + "id": "aw_ru_patch_awapijson", + "label": "awApiJson()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L448", + "community": 9 + }, + { + "id": "aw_ru_patch_ensureawbucket", + "label": "ensureAwBucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L466", + "community": 9 + }, + { + "id": "aw_ru_patch_saveawheartbeat", + "label": "saveAwHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L477", + "community": 9 + }, + { + "id": "aw_ru_patch_loadbucketevents", + "label": "loadBucketEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L485", + "community": 5 + }, + { + "id": "aw_ru_patch_getrulematchfields", + "label": "getRuleMatchFields()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L493", + "community": 14 + }, + { + "id": "aw_ru_patch_serializerulematch", + "label": "serializeRuleMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L505", + "community": 5 + }, + { + "id": "aw_ru_patch_getruleid", + "label": "getRuleId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L517", + "community": 5 + }, + { + "id": "aw_ru_patch_getreviewid", + "label": "getReviewId()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L526", + "community": 5 + }, + { + "id": "aw_ru_patch_collapseruleevents", + "label": "collapseRuleEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L537", + "community": 5 + }, + { + "id": "aw_ru_patch_collapsereviewevents", + "label": "collapseReviewEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L550", + "community": 5 + }, + { + "id": "aw_ru_patch_rulematchesevent", + "label": "ruleMatchesEvent()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L563", + "community": 14 + }, + { + "id": "aw_ru_patch_getsuppressionstate", + "label": "getSuppressionState()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L572", + "community": 5 + }, + { + "id": "aw_ru_patch_removebaddlplinks", + "label": "removeBadDlpLinks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L585", + "community": 2 + }, + { + "id": "aw_ru_patch_updatedlplinks", + "label": "updateDlpLinks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L593", + "community": 2 + }, + { + "id": "aw_ru_patch_builddlpnavitem", + "label": "buildDlpNavItem()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L602", + "community": 2 + }, + { + "id": "aw_ru_patch_findprimarynavlist", + "label": "findPrimaryNavList()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L620", + "community": 2 + }, + { + "id": "aw_ru_patch_injectdlpnavigation", + "label": "injectDlpNavigation()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L633", + "community": 2 + }, + { + "id": "aw_ru_patch_ishomeroute", + "label": "isHomeRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L665", + "community": 8 + }, + { + "id": "aw_ru_patch_getdefaulthostgroupsconfig", + "label": "getDefaultHostGroupsConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L670", + "community": 8 + }, + { + "id": "aw_ru_patch_gethostgroupsstate", + "label": "getHostGroupsState()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L699", + "community": 8 + }, + { + "id": "aw_ru_patch_ensurehostgroupsdata", + "label": "ensureHostGroupsData()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L710", + "community": 8 + }, + { + "id": "aw_ru_patch_ispveactivityroute", + "label": "isPveActivityRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L732", + "community": 2 + }, + { + "id": "aw_ru_patch_extracthostfrombucket", + "label": "extractHostFromBucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L738", + "community": 8 + }, + { + "id": "aw_ru_patch_buildhostbucketmap", + "label": "buildHostBucketMap()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L757", + "community": 8 + }, + { + "id": "aw_ru_patch_matchhostgroup", + "label": "matchHostGroup()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L773", + "community": 8 + }, + { + "id": "aw_ru_patch_buildhostlink", + "label": "buildHostLink()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L788", + "community": 8 + }, + { + "id": "aw_ru_patch_renderhostgroupcards", + "label": "renderHostGroupCards()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L804", + "community": 8 + }, + { + "id": "aw_ru_patch_injecthostgroupscenter", + "label": "injectHostGroupsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L860", + "community": 8 + }, + { + "id": "aw_ru_patch_renderdlptablerows", + "label": "renderDlpTableRows()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L881", + "community": 5 + }, + { + "id": "aw_ru_patch_savedlpreview", + "label": "saveDlpReview()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L922", + "community": 9 + }, + { + "id": "aw_ru_patch_savedlpincident", + "label": "saveDlpIncident()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L956", + "community": 9 + }, + { + "id": "aw_ru_patch_savedlprule", + "label": "saveDlpRule()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L980", + "community": 9 + }, + { + "id": "aw_ru_patch_binddlprowactions", + "label": "bindDlpRowActions()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1001", + "community": 5 + }, + { + "id": "aw_ru_patch_setdlpruleenabled", + "label": "setDlpRuleEnabled()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1036", + "community": 5 + }, + { + "id": "aw_ru_patch_setdlpreviewarchived", + "label": "setDlpReviewArchived()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1055", + "community": 9 + }, + { + "id": "aw_ru_patch_renderdlprulemanager", + "label": "renderDlpRuleManager()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1078", + "community": 5 + }, + { + "id": "aw_ru_patch_renderdlpreviewmanager", + "label": "renderDlpReviewManager()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1123", + "community": 5 + }, + { + "id": "aw_ru_patch_refreshdlpcenter", + "label": "refreshDlpCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1169", + "community": 5 + }, + { + "id": "aw_ru_patch_injectdlpreviewcenter", + "label": "injectDlpReviewCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1198", + "community": 5 + }, + { + "id": "aw_ru_patch_refreshdlpalertscenter", + "label": "refreshDlpAlertsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1267", + "community": 13 + }, + { + "id": "aw_ru_patch_refreshpveauditcenter", + "label": "refreshPveAuditCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1297", + "community": 2 + }, + { + "id": "aw_ru_patch_injectpveauditcenter", + "label": "injectPveAuditCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1343", + "community": 2 + }, + { + "id": "aw_ru_patch_injectdlpalertscenter", + "label": "injectDlpAlertsCenter()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1381", + "community": 13 + }, + { + "id": "aw_ru_patch_gettrendshostfromsettings", + "label": "getTrendsHostFromSettings()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1424", + "community": 2 + }, + { + "id": "aw_ru_patch_gettrendspath", + "label": "getTrendsPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1431", + "community": 2 + }, + { + "id": "aw_ru_patch_shouldredirecttrends", + "label": "shouldRedirectTrends()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1437", + "community": 2 + }, + { + "id": "aw_ru_patch_redirectbaretrendsroute", + "label": "redirectBareTrendsRoute()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1442", + "community": 2 + }, + { + "id": "aw_ru_patch_ensuresettingshost", + "label": "ensureSettingsHost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1465", + "community": 2 + }, + { + "id": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "label": "getPreferredWindowHostFromBuckets()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1483", + "community": 2 + }, + { + "id": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "label": "rewriteUnknownCategoryBuilderQueryBody()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1501", + "community": 2 + }, + { + "id": "aw_ru_patch_installcategorybuildernetworkpatch", + "label": "installCategoryBuilderNetworkPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1527", + "community": 2 + }, + { + "id": "aw_ru_patch_patchcategorybuilderhostlabel", + "label": "patchCategoryBuilderHostLabel()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1572", + "community": 2 + }, + { + "id": "aw_ru_patch_patchactivityheading", + "label": "patchActivityHeading()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1589", + "community": 2 + }, + { + "id": "aw_ru_patch_applypatch", + "label": "applyPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1601", + "community": 2 + }, + { + "id": "aw_ru_patch_scheduleapplypatch", + "label": "scheduleApplyPatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1620", + "community": 2 + }, + { + "id": "install_kit_awindows_20260427_211240_aw_server_aw_sw_cleanup_js", + "label": "aw-sw-cleanup.js", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js", + "source_location": "L1", + "community": 16 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "label": "browser-domains-native-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L1", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L42", + "community": 3 + }, + { + "id": "browser_domains_native_collector_write_collectorlog", + "label": "Write-CollectorLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L109", + "community": 3 + }, + { + "id": "browser_domains_native_collector_write_dlpincidentlog", + "label": "Write-DlpIncidentLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L123", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_domainmatch", + "label": "Test-DomainMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L137", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_hostfromurl", + "label": "Get-HostFromUrl()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L152", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_rootdomain", + "label": "Get-RootDomain()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L173", + "community": 3 + }, + { + "id": "browser_domains_native_collector_convertto_normalizedurl", + "label": "ConvertTo-NormalizedUrl()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L194", + "community": 3 + }, + { + "id": "browser_domains_native_collector_load_customcategoryrules", + "label": "Load-CustomCategoryRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L225", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_webcategory", + "label": "Get-WebCategory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L274", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_domainlistmatch", + "label": "Test-DomainListMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L296", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_dlpruletimewindow", + "label": "Test-DlpRuleTimeWindow()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L315", + "community": 3 + }, + { + "id": "browser_domains_native_collector_load_dlppolicy", + "label": "Load-DlpPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L337", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_dlprulematch", + "label": "Test-DlpRuleMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L398", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_dlpdecision", + "label": "Get-DlpDecision()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L454", + "community": 3 + }, + { + "id": "browser_domains_native_collector_should_emitincident", + "label": "Should-EmitIncident()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L478", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_dlpincidentheartbeat", + "label": "Send-DlpIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L496", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_filesha256hex", + "label": "Get-FileSha256Hex()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L547", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_directory", + "label": "Ensure-Directory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L565", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_incidentscreenshotpath", + "label": "Get-IncidentScreenshotPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L572", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_screenshottypesloaded", + "label": "Ensure-ScreenshotTypesLoaded()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L587", + "community": 3 + }, + { + "id": "browser_domains_native_collector_capture_incidentscreenshot", + "label": "Capture-IncidentScreenshot()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L596", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_foregroundwindowcontext", + "label": "Get-ForegroundWindowContext()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L633", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_browserurlfromwindow", + "label": "Get-BrowserUrlFromWindow()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L661", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L693", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_heartbeat", + "label": "Send-Heartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L714", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_categoryheartbeat", + "label": "Send-CategoryHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L739", + "community": 3 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_deploy_domain_users_ps1", + "label": "deploy-domain-users.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1", + "source_location": "L1", + "community": 17 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_deploy_ensemble_ps1", + "label": "deploy-ensemble.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1", + "source_location": "L1", + "community": 18 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_deploy_single_user_ps1", + "label": "deploy-single-user.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1", + "source_location": "L1", + "community": 19 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "label": "dlp-endpoint-signals-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L1", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L16", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_write_endpointlog", + "label": "Write-EndpointLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L24", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L36", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L46", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "label": "Send-EndpointSignalHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L67", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "label": "Send-DlpIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L91", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_filesha256hex", + "label": "Get-FileSha256Hex()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L132", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_directory", + "label": "Ensure-Directory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L150", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "label": "Get-IncidentScreenshotPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L157", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "label": "Ensure-ScreenshotTypesLoaded()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L172", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "label": "Capture-IncidentScreenshot()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L181", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_show_enforcementnotification", + "label": "Show-EnforcementNotification()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L222", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "label": "Invoke-ClipboardEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L244", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "label": "Invoke-UsbWriteBlockEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L258", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "label": "Invoke-PrintJobEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L282", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_stringhash", + "label": "Get-StringHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L311", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_load_dlppolicy", + "label": "Load-DlpPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L324", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_should_emitbycooldown", + "label": "Should-EmitByCooldown()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L366", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "label": "Evaluate-ClipboardRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L384", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_usbrules", + "label": "Evaluate-UsbRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L432", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_printrules", + "label": "Evaluate-PrintRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L467", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "label": "Test-LooksLikeMojibakeQuestionMarks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L513", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "label": "Normalize-OwnerForMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L519", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_ownerloosematch", + "label": "Test-OwnerLooseMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L538", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_normalize_printerformatch", + "label": "Normalize-PrinterForMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L551", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_printerloosematch", + "label": "Test-PrinterLooseMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L564", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "label": "Get-PrintServiceEventSummary()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L577", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "label": "Get-PrintServiceDocumentFallback()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L597", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "label": "Write-PrintServiceEventTrace()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L647", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "label": "Get-BetterDocumentNameFromPrintServiceEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L676", + "community": 1 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "label": "email-outbound-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L1", + "community": 4 + }, + { + "id": "email_outbound_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L39", + "community": 4 + }, + { + "id": "email_outbound_collector_write_collectorlog", + "label": "Write-CollectorLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L47", + "community": 4 + }, + { + "id": "email_outbound_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L56", + "community": 4 + }, + { + "id": "email_outbound_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L65", + "community": 4 + }, + { + "id": "email_outbound_collector_get_stringhash", + "label": "Get-StringHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L81", + "community": 4 + }, + { + "id": "email_outbound_collector_send_emailheartbeat", + "label": "Send-EmailHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L92", + "community": 4 + }, + { + "id": "email_outbound_collector_send_emailincidentheartbeat", + "label": "Send-EmailIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L113", + "community": 4 + }, + { + "id": "email_outbound_collector_show_enforcementnotification", + "label": "Show-EnforcementNotification()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L141", + "community": 4 + }, + { + "id": "email_outbound_collector_load_emailpolicy", + "label": "Load-EmailPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L165", + "community": 4 + }, + { + "id": "email_outbound_collector_should_emitbycooldown", + "label": "Should-EmitByCooldown()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L202", + "community": 4 + }, + { + "id": "email_outbound_collector_evaluate_emailrules", + "label": "Evaluate-EmailRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L222", + "community": 4 + }, + { + "id": "email_outbound_collector_invoke_emailenforcement", + "label": "Invoke-EmailEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L302", + "community": 4 + }, + { + "id": "email_outbound_collector_initialize_outlookcom", + "label": "Initialize-OutlookCom()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L324", + "community": 4 + }, + { + "id": "email_outbound_collector_get_outlooksentitems", + "label": "Get-OutlookSentItems()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L338", + "community": 4 + }, + { + "id": "email_outbound_collector_poll_outlooksentitems", + "label": "Poll-OutlookSentItems()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L387", + "community": 4 + }, + { + "id": "email_outbound_collector_poll_smtpconnections", + "label": "Poll-SmtpConnections()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L437", + "community": 4 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_hardening_recovery_ps1", + "label": "hardening-recovery.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1", + "source_location": "L1", + "community": 20 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "label": "migrate-awatch-rus-paths.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L1", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_copy_directorycontents", + "label": "Copy-DirectoryContents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L19", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_copy_ifexists", + "label": "Copy-IfExists()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L35", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_convert_pathvalue", + "label": "Convert-PathValue()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L48", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_stop_awatchtaskset", + "label": "Stop-AWatchTaskSet()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L61", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_get_existingawatchconfig", + "label": "Get-ExistingAWatchConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L67", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "label": "Update-AWatchConfigPaths()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L88", + "community": 10 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_validate_deployment_ps1", + "label": "validate-deployment.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1", + "source_location": "L1", + "community": 21 + }, + { + "id": "install_kit_awindows_20260427_211240_windows_worktime_session_collector_ps1", + "label": "worktime-session-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L1", + "community": 11 + }, + { + "id": "worktime_session_collector_get_config", + "label": "Get-Config()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L10", + "community": 11 + }, + { + "id": "worktime_session_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L20", + "community": 11 + }, + { + "id": "worktime_session_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L30", + "community": 11 + }, + { + "id": "worktime_session_collector_get_sessionrecords", + "label": "Get-SessionRecords()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L53", + "community": 11 + }, + { + "id": "pfsense_pfsense_aw_poller_py", + "label": "pfsense-aw-poller.py", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L1", + "community": 7 + }, + { + "id": "pfsense_aw_poller_utc_now_iso", + "label": "utc_now_iso()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L14", + "community": 7 + }, + { + "id": "pfsense_aw_poller_load_json", + "label": "load_json()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L18", + "community": 7 + }, + { + "id": "pfsense_aw_poller_build_ssl_context", + "label": "build_ssl_context()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L22", + "community": 7 + }, + { + "id": "pfsense_aw_poller_http_json", + "label": "http_json()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L31", + "community": 7 + }, + { + "id": "pfsense_aw_poller_ensure_bucket", + "label": "ensure_bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L44", + "community": 7 + }, + { + "id": "pfsense_aw_poller_send_heartbeat", + "label": "send_heartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L58", + "community": 7 + }, + { + "id": "pfsense_aw_poller_normalize_headers", + "label": "normalize_headers()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L68", + "community": 7 + }, + { + "id": "pfsense_aw_poller_summarize_payload", + "label": "summarize_payload()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L82", + "community": 7 + }, + { + "id": "pfsense_aw_poller_poll_endpoint", + "label": "poll_endpoint()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L100", + "community": 7 + }, + { + "id": "pfsense_aw_poller_run_once", + "label": "run_once()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L173", + "community": 7 + }, + { + "id": "pfsense_aw_poller_main", + "label": "main()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L187", + "community": 7 + }, + { + "id": "scripts_aggregate_dlp_events_py", + "label": "aggregate_dlp_events.py", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L1", + "community": 0 + }, + { + "id": "aggregate_dlp_events_bucket", + "label": "Bucket", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L49", + "community": 0 + }, + { + "id": "aggregate_dlp_events_awevent", + "label": "AwEvent", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L57", + "community": 0 + }, + { + "id": "aggregate_dlp_events_psycopgconnection", + "label": "PsycopgConnection", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L67", + "community": 0 + }, + { + "id": "protocol", + "label": "Protocol", + "file_type": "code", + "source_file": "", + "source_location": "", + "community": 0 + }, + { + "id": "aggregate_dlp_events_psycopgconnection_cursor", + "label": ".cursor()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L68", + "community": 0 + }, + { + "id": "aggregate_dlp_events_psycopgconnection_commit", + "label": ".commit()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L71", + "community": 0 + }, + { + "id": "aggregate_dlp_events_utc_now", + "label": "utc_now()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L75", + "community": 0 + }, + { + "id": "aggregate_dlp_events_parse_timestamp", + "label": "parse_timestamp()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L79", + "community": 0 + }, + { + "id": "aggregate_dlp_events_format_aw_timestamp", + "label": "format_aw_timestamp()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L87", + "community": 0 + }, + { + "id": "aggregate_dlp_events_load_state", + "label": "load_state()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L91", + "community": 0 + }, + { + "id": "aggregate_dlp_events_save_state", + "label": "save_state()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L97", + "community": 0 + }, + { + "id": "aggregate_dlp_events_normalize_base_url", + "label": "normalize_base_url()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L102", + "community": 0 + }, + { + "id": "aggregate_dlp_events_aw_get_json", + "label": "aw_get_json()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L106", + "community": 0 + }, + { + "id": "aggregate_dlp_events_list_buckets", + "label": "list_buckets()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L113", + "community": 0 + }, + { + "id": "aggregate_dlp_events_bucket_stream_type", + "label": "bucket_stream_type()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L132", + "community": 0 + }, + { + "id": "aggregate_dlp_events_select_buckets", + "label": "select_buckets()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L140", + "community": 0 + }, + { + "id": "aggregate_dlp_events_build_events_path", + "label": "build_events_path()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L149", + "community": 0 + }, + { + "id": "aggregate_dlp_events_event_key", + "label": "event_key()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L160", + "community": 0 + }, + { + "id": "aggregate_dlp_events_fetch_bucket_events", + "label": "fetch_bucket_events()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L165", + "community": 0 + }, + { + "id": "aggregate_dlp_events_connect_sqlite", + "label": "connect_sqlite()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L201", + "community": 0 + }, + { + "id": "aggregate_dlp_events_ensure_schema", + "label": "ensure_schema()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L210", + "community": 0 + }, + { + "id": "aggregate_dlp_events_ensure_postgres_schema", + "label": "ensure_postgres_schema()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L258", + "community": 0 + }, + { + "id": "aggregate_dlp_events_first_string", + "label": "first_string()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L307", + "community": 0 + }, + { + "id": "aggregate_dlp_events_bool_as_int", + "label": "bool_as_int()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L315", + "community": 0 + }, + { + "id": "aggregate_dlp_events_event_row", + "label": "event_row()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L323", + "community": 0 + }, + { + "id": "aggregate_dlp_events_insert_events", + "label": "insert_events()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L351", + "community": 0 + }, + { + "id": "aggregate_dlp_events_insert_postgres_events", + "label": "insert_postgres_events()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L389", + "community": 0 + }, + { + "id": "aggregate_dlp_events_get_start_time", + "label": "get_start_time()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L416", + "community": 0 + }, + { + "id": "aggregate_dlp_events_parse_prefixes", + "label": "parse_prefixes()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L424", + "community": 0 + }, + { + "id": "aggregate_dlp_events_build_parser", + "label": "build_parser()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L431", + "community": 0 + }, + { + "id": "aggregate_dlp_events_main", + "label": "main()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L447", + "community": 0 + }, + { + "id": "scripts_merge_aw_server_dbs_py", + "label": "merge_aw_server_dbs.py", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L1", + "community": 0 + }, + { + "id": "merge_aw_server_dbs_connect", + "label": "connect()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L10", + "community": 0 + }, + { + "id": "merge_aw_server_dbs_bucket_key", + "label": "bucket_key()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L17", + "community": 0 + }, + { + "id": "merge_aw_server_dbs_ensure_parent", + "label": "ensure_parent()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L26", + "community": 0 + }, + { + "id": "merge_aw_server_dbs_load_existing_events", + "label": "load_existing_events()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L30", + "community": 0 + }, + { + "id": "merge_aw_server_dbs_main", + "label": "main()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L38", + "community": 0 + }, + { + "id": "windows_browser_domains_native_collector_ps1", + "label": "browser-domains-native-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L1", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L42", + "community": 3 + }, + { + "id": "browser_domains_native_collector_write_collectorlog", + "label": "Write-CollectorLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L109", + "community": 3 + }, + { + "id": "browser_domains_native_collector_write_dlpincidentlog", + "label": "Write-DlpIncidentLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L123", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_domainmatch", + "label": "Test-DomainMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L137", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_hostfromurl", + "label": "Get-HostFromUrl()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L152", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_rootdomain", + "label": "Get-RootDomain()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L173", + "community": 3 + }, + { + "id": "browser_domains_native_collector_convertto_normalizedurl", + "label": "ConvertTo-NormalizedUrl()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L194", + "community": 3 + }, + { + "id": "browser_domains_native_collector_load_customcategoryrules", + "label": "Load-CustomCategoryRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L225", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_webcategory", + "label": "Get-WebCategory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L274", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_domainlistmatch", + "label": "Test-DomainListMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L296", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_dlpruletimewindow", + "label": "Test-DlpRuleTimeWindow()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L315", + "community": 3 + }, + { + "id": "browser_domains_native_collector_load_dlppolicy", + "label": "Load-DlpPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L337", + "community": 3 + }, + { + "id": "browser_domains_native_collector_test_dlprulematch", + "label": "Test-DlpRuleMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L398", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_dlpdecision", + "label": "Get-DlpDecision()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L454", + "community": 3 + }, + { + "id": "browser_domains_native_collector_should_emitincident", + "label": "Should-EmitIncident()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L478", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_dlpincidentheartbeat", + "label": "Send-DlpIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L496", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_filesha256hex", + "label": "Get-FileSha256Hex()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L547", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_directory", + "label": "Ensure-Directory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L565", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_incidentscreenshotpath", + "label": "Get-IncidentScreenshotPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L572", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_screenshottypesloaded", + "label": "Ensure-ScreenshotTypesLoaded()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L587", + "community": 3 + }, + { + "id": "browser_domains_native_collector_capture_incidentscreenshot", + "label": "Capture-IncidentScreenshot()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L596", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_foregroundwindowcontext", + "label": "Get-ForegroundWindowContext()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L633", + "community": 3 + }, + { + "id": "browser_domains_native_collector_get_browserurlfromwindow", + "label": "Get-BrowserUrlFromWindow()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L661", + "community": 3 + }, + { + "id": "browser_domains_native_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L693", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_heartbeat", + "label": "Send-Heartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L714", + "community": 3 + }, + { + "id": "browser_domains_native_collector_send_categoryheartbeat", + "label": "Send-CategoryHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L739", + "community": 3 + }, + { + "id": "windows_deploy_domain_users_ps1", + "label": "deploy-domain-users.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-domain-users.ps1", + "source_location": "L1", + "community": 22 + }, + { + "id": "windows_deploy_ensemble_ps1", + "label": "deploy-ensemble.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-ensemble.ps1", + "source_location": "L1", + "community": 23 + }, + { + "id": "windows_deploy_single_user_ps1", + "label": "deploy-single-user.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-single-user.ps1", + "source_location": "L1", + "community": 24 + }, + { + "id": "windows_dlp_endpoint_signals_collector_ps1", + "label": "dlp-endpoint-signals-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L1", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L16", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_write_endpointlog", + "label": "Write-EndpointLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L24", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L36", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L46", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "label": "Send-EndpointSignalHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L67", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "label": "Send-DlpIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L91", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_filesha256hex", + "label": "Get-FileSha256Hex()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L132", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_directory", + "label": "Ensure-Directory()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L150", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "label": "Get-IncidentScreenshotPath()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L157", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "label": "Ensure-ScreenshotTypesLoaded()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L172", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "label": "Capture-IncidentScreenshot()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L181", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_show_enforcementnotification", + "label": "Show-EnforcementNotification()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L222", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "label": "Invoke-ClipboardEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L244", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "label": "Invoke-UsbWriteBlockEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L258", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "label": "Invoke-PrintJobEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L282", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_stringhash", + "label": "Get-StringHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L311", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_load_dlppolicy", + "label": "Load-DlpPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L324", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_should_emitbycooldown", + "label": "Should-EmitByCooldown()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L366", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "label": "Evaluate-ClipboardRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L384", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_usbrules", + "label": "Evaluate-UsbRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L432", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_evaluate_printrules", + "label": "Evaluate-PrintRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L467", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "label": "Test-LooksLikeMojibakeQuestionMarks()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L513", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "label": "Normalize-OwnerForMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L519", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_ownerloosematch", + "label": "Test-OwnerLooseMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L538", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_normalize_printerformatch", + "label": "Normalize-PrinterForMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L551", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_test_printerloosematch", + "label": "Test-PrinterLooseMatch()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L564", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "label": "Get-PrintServiceEventSummary()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L577", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "label": "Get-PrintServiceDocumentFallback()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L597", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "label": "Write-PrintServiceEventTrace()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L647", + "community": 1 + }, + { + "id": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "label": "Get-BetterDocumentNameFromPrintServiceEvents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L676", + "community": 1 + }, + { + "id": "windows_email_outbound_collector_ps1", + "label": "email-outbound-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L1", + "community": 4 + }, + { + "id": "email_outbound_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L39", + "community": 4 + }, + { + "id": "email_outbound_collector_write_collectorlog", + "label": "Write-CollectorLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L47", + "community": 4 + }, + { + "id": "email_outbound_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L56", + "community": 4 + }, + { + "id": "email_outbound_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L65", + "community": 4 + }, + { + "id": "email_outbound_collector_get_stringhash", + "label": "Get-StringHash()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L81", + "community": 4 + }, + { + "id": "email_outbound_collector_send_emailheartbeat", + "label": "Send-EmailHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L92", + "community": 4 + }, + { + "id": "email_outbound_collector_send_emailincidentheartbeat", + "label": "Send-EmailIncidentHeartbeat()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L113", + "community": 4 + }, + { + "id": "email_outbound_collector_show_enforcementnotification", + "label": "Show-EnforcementNotification()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L141", + "community": 4 + }, + { + "id": "email_outbound_collector_load_emailpolicy", + "label": "Load-EmailPolicy()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L165", + "community": 4 + }, + { + "id": "email_outbound_collector_should_emitbycooldown", + "label": "Should-EmitByCooldown()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L202", + "community": 4 + }, + { + "id": "email_outbound_collector_evaluate_emailrules", + "label": "Evaluate-EmailRules()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L222", + "community": 4 + }, + { + "id": "email_outbound_collector_invoke_emailenforcement", + "label": "Invoke-EmailEnforcement()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L302", + "community": 4 + }, + { + "id": "email_outbound_collector_initialize_outlookcom", + "label": "Initialize-OutlookCom()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L324", + "community": 4 + }, + { + "id": "email_outbound_collector_get_outlooksentitems", + "label": "Get-OutlookSentItems()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L338", + "community": 4 + }, + { + "id": "email_outbound_collector_poll_outlooksentitems", + "label": "Poll-OutlookSentItems()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L387", + "community": 4 + }, + { + "id": "email_outbound_collector_poll_smtpconnections", + "label": "Poll-SmtpConnections()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L437", + "community": 4 + }, + { + "id": "windows_file_operations_collector_ps1", + "label": "file-operations-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L1", + "community": 12 + }, + { + "id": "file_operations_collector_get_deploymentconfig", + "label": "Get-DeploymentConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L30", + "community": 12 + }, + { + "id": "file_operations_collector_write_filecollectorlog", + "label": "Write-FileCollectorLog()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L38", + "community": 12 + }, + { + "id": "file_operations_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L46", + "community": 12 + }, + { + "id": "file_operations_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L61", + "community": 12 + }, + { + "id": "file_operations_collector_send_fileoperationevent", + "label": "Send-FileOperationEvent()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L77", + "community": 12 + }, + { + "id": "windows_hardening_recovery_ps1", + "label": "hardening-recovery.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/hardening-recovery.ps1", + "source_location": "L1", + "community": 25 + }, + { + "id": "windows_migrate_awatch_rus_paths_ps1", + "label": "migrate-awatch-rus-paths.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L1", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_copy_directorycontents", + "label": "Copy-DirectoryContents()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L19", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_copy_ifexists", + "label": "Copy-IfExists()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L35", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_convert_pathvalue", + "label": "Convert-PathValue()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L48", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_stop_awatchtaskset", + "label": "Stop-AWatchTaskSet()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L61", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_get_existingawatchconfig", + "label": "Get-ExistingAWatchConfig()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L67", + "community": 10 + }, + { + "id": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "label": "Update-AWatchConfigPaths()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L88", + "community": 10 + }, + { + "id": "windows_validate_deployment_ps1", + "label": "validate-deployment.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/validate-deployment.ps1", + "source_location": "L1", + "community": 26 + }, + { + "id": "windows_worktime_session_collector_ps1", + "label": "worktime-session-collector.ps1", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L1", + "community": 11 + }, + { + "id": "worktime_session_collector_get_config", + "label": "Get-Config()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L10", + "community": 11 + }, + { + "id": "worktime_session_collector_invoke_awjsonpost", + "label": "Invoke-AwJsonPost()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L20", + "community": 11 + }, + { + "id": "worktime_session_collector_ensure_bucket", + "label": "Ensure-Bucket()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L30", + "community": 11 + }, + { + "id": "worktime_session_collector_get_sessionrecords", + "label": "Get-SessionRecords()", + "file_type": "code", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L53", + "community": 11 + } + ], + "edges": [ + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_replacetext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L269", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_walk", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L281", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_translateattributes", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L293", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectstyles", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L305", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_hidenoisenavigation", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L348", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L360", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ispvelikehost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L369", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_islikelyclienthost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L373", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L383", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L397", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphref", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L405", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_isdlpsignalbucketroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L410", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_isalertsroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L414", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L418", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L424", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_escapehtml", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L429", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_normalizetext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L438", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_builddlpkey", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L442", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_generatedlpid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_awapijson", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L458", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensureawbucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L476", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L487", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_loadbucketevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L495", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getrulematchfields", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L503", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_serializerulematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L515", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getruleid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L527", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getreviewid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L536", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_collapseruleevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L547", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L560", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_rulematchesevent", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L573", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L582", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_removebaddlplinks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L595", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_updatedlplinks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L603", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_builddlpnavitem", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L612", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_findprimarynavlist", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L630", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L643", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ishomeroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L675", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L680", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L722", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L733", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ispveactivityroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L755", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_extracthostfrombucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L761", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_buildhostbucketmap", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L788", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_hosthasbucketprefix", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L804", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_matchhostgroup", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L810", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_buildhostlink", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L839", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderhostgroupcards", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L855", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injecthostgroupscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L911", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L932", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlpreview", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L973", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlpincident", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1007", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlprule", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1031", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_binddlprowactions", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1052", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_setdlpruleenabled", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1087", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_setdlpreviewarchived", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1106", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1129", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1174", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshdlpcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1220", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpreviewcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1249", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshdlpalertscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1318", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshpveauditcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1348", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectpveauditcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1394", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpalertscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1432", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1475", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gettrendspath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1483", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_shouldredirecttrends", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1489", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_redirectbaretrendsroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1494", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensuresettingshost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1517", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1535", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1552", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_installcategorybuildernetworkpatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1578", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_patchcategorybuilderhostlabel", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1625", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_patchactivityheading", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1642", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_applypatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1654", + "weight": 1.0 + }, + { + "source": "aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_scheduleapplypatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1673", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_walk", + "target": "aw_ru_patch_replacetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L286", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_translateattributes", + "target": "aw_ru_patch_replacetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L299", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L390", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L398", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_islikelyclienthost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L399", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L400", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L400", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L402", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensureawbucket", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L477", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_saveawheartbeat", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L489", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_loadbucketevents", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L496", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getrulematchfields", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L506", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getruleid", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L529", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getruleid", + "target": "aw_ru_patch_serializerulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L530", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getreviewid", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L539", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getreviewid", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L541", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_collapseruleevents", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L553", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_collapsereviewevents", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L566", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rulematchesevent", + "target": "aw_ru_patch_getrulematchfields", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L574", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rulematchesevent", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L577", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_findprimarynavlist", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L634", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L644", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L645", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_removebaddlplinks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L646", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_getdlphref", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L651", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_updatedlplinks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L653", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_findprimarynavlist", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L656", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_builddlpnavitem", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L659", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L734", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L741", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L743", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ispveactivityroute", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L758", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_buildhostbucketmap", + "target": "aw_ru_patch_extracthostfrombucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L796", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_matchhostgroup", + "target": "aw_ru_patch_hosthasbucketprefix", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L812", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L856", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_buildhostbucketmap", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L858", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_matchhostgroup", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L867", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_buildhostlink", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L876", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L877", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_ishomeroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L912", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L928", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_renderhostgroupcards", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L929", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L933", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_rulematchesevent", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L939", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L942", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L944", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_binddlprowactions", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L968", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L969", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L970", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L975", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L979", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L984", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L990", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_savedlpincident", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L999", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1009", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1010", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1015", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1021", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1033", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1037", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1042", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_getrulematchfields", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1047", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1053", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1059", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_savedlpreview", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1064", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1065", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1065", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1066", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_savedlprule", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1075", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1076", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1089", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1091", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1096", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1108", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1111", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1116", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1119", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1130", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1140", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1140", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_serializerulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1145", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_setdlpruleenabled", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1162", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1163", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1163", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1165", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1175", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1187", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1187", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_setdlpreviewarchived", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1209", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1210", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1210", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1211", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1221", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1226", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1226", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1229", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1236", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1240", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_isdlpsignalbucketroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1250", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1251", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1252", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1266", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_refreshdlpcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1299", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1302", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1305", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1308", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpalertscenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1321", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpalertscenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1328", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshpveauditcenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1354", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshpveauditcenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1380", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_ispveactivityroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1395", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1396", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_refreshpveauditcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1428", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_isalertsroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1433", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1434", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1448", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_refreshdlpalertscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1460", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_gettrendshostfromsettings", + "target": "aw_ru_patch_islikelyclienthost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1480", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_shouldredirecttrends", + "target": "aw_ru_patch_gettrendspath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1490", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_shouldredirecttrends", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1495", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1503", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1504", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensuresettingshost", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1526", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensuresettingshost", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1531", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1536", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1538", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "target": "aw_ru_patch_islikelyclienthost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1547", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1560", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_installcategorybuildernetworkpatch", + "target": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1590", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_patchcategorybuilderhostlabel", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1627", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1655", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_ensuresettingshost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1656", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1657", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_installcategorybuildernetworkpatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1658", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectstyles", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1659", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_walk", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1660", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_translateattributes", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1661", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_hidenoisenavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1662", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_patchactivityheading", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1663", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_patchcategorybuilderhostlabel", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1664", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectpveauditcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1665", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1666", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpreviewcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1667", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpalertscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1668", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injecthostgroupscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1669", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_redirectbaretrendsroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1670", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_scheduleapplypatch", + "target": "aw_ru_patch_applypatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/aw-ru-patch.js", + "source_location": "L1678", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "time", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L7", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "logging", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L8", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "requests", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L9", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "prometheus_client", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "datetime", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L11", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "aw_activitywatch_activitywatchexporter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L31", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter", + "target": "aw_activitywatch_activitywatchexporter_init", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L32", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter", + "target": "aw_activitywatch_activitywatchexporter_get_buckets", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L38", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter", + "target": "aw_activitywatch_activitywatchexporter_get_bucket_events", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L48", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter", + "target": "aw_activitywatch_activitywatchexporter_get_bucket_info", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L62", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter", + "target": "aw_activitywatch_activitywatchexporter_collect_metrics", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L72", + "weight": 1.0 + }, + { + "source": "grafana_1c_sql_exporter_collectors_aw_activitywatch_py", + "target": "aw_activitywatch_main", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L125", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter_collect_metrics", + "target": "aw_activitywatch_activitywatchexporter_get_buckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L74", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_activitywatchexporter_collect_metrics", + "target": "aw_activitywatch_activitywatchexporter_get_bucket_events", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L95", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_main", + "target": "aw_activitywatch_activitywatchexporter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L126", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_main", + "target": "aw_activitywatch_activitywatchexporter_collect_metrics", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L129", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_rationale_39", + "target": "aw_activitywatch_activitywatchexporter_get_buckets", + "relation": "rationale_for", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L39", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_rationale_49", + "target": "aw_activitywatch_activitywatchexporter_get_bucket_events", + "relation": "rationale_for", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L49", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_rationale_63", + "target": "aw_activitywatch_activitywatchexporter_get_bucket_info", + "relation": "rationale_for", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L63", + "weight": 1.0 + }, + { + "source": "aw_activitywatch_rationale_73", + "target": "aw_activitywatch_activitywatchexporter_collect_metrics", + "relation": "rationale_for", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/aw_activitywatch.py", + "source_location": "L73", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_replacetext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L269", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_walk", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L281", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_translateattributes", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L293", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectstyles", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L305", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_hidenoisenavigation", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L348", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L360", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ispvelikehost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L369", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L373", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L387", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphref", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L395", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_isdlpsignalbucketroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L400", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_isalertsroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L404", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L408", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L414", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_escapehtml", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L419", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_normalizetext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L428", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_builddlpkey", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L432", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_generatedlpid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L444", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_awapijson", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L448", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensureawbucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L466", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L477", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_loadbucketevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L485", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getrulematchfields", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L493", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_serializerulematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L505", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getruleid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L517", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getreviewid", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L526", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_collapseruleevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L537", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L550", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_rulematchesevent", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L563", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L572", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_removebaddlplinks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L585", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_updatedlplinks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L593", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_builddlpnavitem", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L602", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_findprimarynavlist", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L620", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L633", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ishomeroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L665", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L670", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L699", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L710", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ispveactivityroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L732", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_extracthostfrombucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L738", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_buildhostbucketmap", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L757", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_matchhostgroup", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L773", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_buildhostlink", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L788", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderhostgroupcards", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L804", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injecthostgroupscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L860", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L881", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlpreview", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L922", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlpincident", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L956", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_savedlprule", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L980", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_binddlprowactions", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1001", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_setdlpruleenabled", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1036", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_setdlpreviewarchived", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1055", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1078", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1123", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshdlpcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1169", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpreviewcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1198", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshdlpalertscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1267", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_refreshpveauditcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1297", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectpveauditcenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1343", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_injectdlpalertscenter", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1381", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1424", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_gettrendspath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1431", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_shouldredirecttrends", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1437", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_redirectbaretrendsroute", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1442", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_ensuresettingshost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1465", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1483", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1501", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_installcategorybuildernetworkpatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1527", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_patchcategorybuilderhostlabel", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1572", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_patchactivityheading", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1589", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_applypatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1601", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_aw_server_aw_ru_patch_js", + "target": "aw_ru_patch_scheduleapplypatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1620", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_walk", + "target": "aw_ru_patch_replacetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L286", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_translateattributes", + "target": "aw_ru_patch_replacetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L299", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L380", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L388", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L390", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L390", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getdlphostfromsettings", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L392", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensureawbucket", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L467", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_saveawheartbeat", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L479", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_loadbucketevents", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L486", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getrulematchfields", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L496", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getruleid", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L519", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getruleid", + "target": "aw_ru_patch_serializerulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L520", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getreviewid", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L529", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getreviewid", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L531", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_collapseruleevents", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L543", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_collapsereviewevents", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L556", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rulematchesevent", + "target": "aw_ru_patch_getrulematchfields", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L564", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rulematchesevent", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L567", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_findprimarynavlist", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L624", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L634", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L635", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_removebaddlplinks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L636", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_getdlphref", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L641", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_updatedlplinks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L643", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_findprimarynavlist", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L646", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpnavigation", + "target": "aw_ru_patch_builddlpnavitem", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L649", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L711", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_awapijson", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L718", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensurehostgroupsdata", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L720", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ispveactivityroute", + "target": "aw_ru_patch_ispvelikehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L735", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_buildhostbucketmap", + "target": "aw_ru_patch_extracthostfrombucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L765", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_getdefaulthostgroupsconfig", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L805", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_buildhostbucketmap", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L807", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_matchhostgroup", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L816", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_buildhostlink", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L825", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderhostgroupcards", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L826", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_ishomeroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L861", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L877", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injecthostgroupscenter", + "target": "aw_ru_patch_renderhostgroupcards", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L878", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L882", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_rulematchesevent", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L888", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L891", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L893", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_binddlprowactions", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L917", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L918", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlptablerows", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L919", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L924", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L928", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L933", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L939", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpreview", + "target": "aw_ru_patch_savedlpincident", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L948", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L958", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L959", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L964", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlpincident", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L970", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L982", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L986", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_generatedlpid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L991", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_savedlprule", + "target": "aw_ru_patch_getrulematchfields", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L996", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1002", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_builddlpkey", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1008", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_savedlpreview", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1013", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1014", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1014", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1015", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_savedlprule", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1024", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_binddlprowactions", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1025", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1038", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1040", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpruleenabled", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1045", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_ensureawbucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1057", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_saveawheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1060", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1065", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_setdlpreviewarchived", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1068", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1079", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1089", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_getruleid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1089", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_serializerulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1094", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_setdlpruleenabled", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1111", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1112", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1112", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlprulemanager", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1114", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1124", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1136", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_getreviewid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1136", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_setdlpreviewarchived", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1158", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1159", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1159", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_renderdlpreviewmanager", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1160", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_getsuppressionstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1170", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1175", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1175", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_collapseruleevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1178", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_collapsereviewevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1185", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpcenter", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1189", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_isdlpsignalbucketroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1199", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_getdlpbucketidfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1200", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_getdlphostfrombucketid", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1201", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1215", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_refreshdlpcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1248", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlptablerows", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1251", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlprulemanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1254", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpreviewcenter", + "target": "aw_ru_patch_renderdlpreviewmanager", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1257", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpalertscenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1270", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshdlpalertscenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1277", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshpveauditcenter", + "target": "aw_ru_patch_loadbucketevents", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1303", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_refreshpveauditcenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1329", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_ispveactivityroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1344", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1345", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectpveauditcenter", + "target": "aw_ru_patch_refreshpveauditcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1377", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_isalertsroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1382", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_getcurrenthostfromhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1383", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_escapehtml", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1397", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_injectdlpalertscenter", + "target": "aw_ru_patch_refreshdlpalertscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1409", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_shouldredirecttrends", + "target": "aw_ru_patch_gettrendspath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1438", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_shouldredirecttrends", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1443", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1451", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_redirectbaretrendsroute", + "target": "aw_ru_patch_gettrendshostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1452", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensuresettingshost", + "target": "aw_ru_patch_getdlphostfromsettings", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1474", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_ensuresettingshost", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1479", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "target": "aw_ru_patch_gethostgroupsstate", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1484", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "target": "aw_ru_patch_normalizetext", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1486", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1509", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_installcategorybuildernetworkpatch", + "target": "aw_ru_patch_rewriteunknowncategorybuilderquerybody", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1538", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_patchcategorybuilderhostlabel", + "target": "aw_ru_patch_getpreferredwindowhostfrombuckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1574", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_enforcesafeactivityviewforpvehost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1602", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_ensuresettingshost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1603", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_ensurehostgroupsdata", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1604", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_installcategorybuildernetworkpatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1605", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectstyles", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1606", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_walk", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1607", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_translateattributes", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1608", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_hidenoisenavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1609", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_patchactivityheading", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1610", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_patchcategorybuilderhostlabel", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1611", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectpveauditcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1612", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpnavigation", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1613", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpreviewcenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1614", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injectdlpalertscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1615", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_injecthostgroupscenter", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1616", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_applypatch", + "target": "aw_ru_patch_redirectbaretrendsroute", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1617", + "weight": 1.0 + }, + { + "source": "aw_ru_patch_scheduleapplypatch", + "target": "aw_ru_patch_applypatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js", + "source_location": "L1625", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L42", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L109", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_write_dlpincidentlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L123", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L137", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_hostfromurl", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L152", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_rootdomain", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L173", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_convertto_normalizedurl", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L194", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_load_customcategoryrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L225", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_webcategory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L274", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_domainlistmatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L296", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_dlpruletimewindow", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L315", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_load_dlppolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L337", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_dlprulematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L398", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_dlpdecision", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_should_emitincident", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L478", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_dlpincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L496", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_filesha256hex", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L547", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_directory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L565", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_incidentscreenshotpath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L572", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_screenshottypesloaded", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L587", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_capture_incidentscreenshot", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L596", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_foregroundwindowcontext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L633", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_browserurlfromwindow", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L661", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L693", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_heartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L714", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_categoryheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L739", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_load_customcategoryrules", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L266", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_webcategory", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L279", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_domainlistmatch", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L307", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_load_dlppolicy", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L341", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_dlprulematch", + "target": "browser_domains_native_collector_test_dlpruletimewindow", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L416", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_dlprulematch", + "target": "browser_domains_native_collector_test_domainlistmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L421", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_dlpdecision", + "target": "browser_domains_native_collector_test_dlprulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L470", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_dlpincidentheartbeat", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L510", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_dlpincidentheartbeat", + "target": "browser_domains_native_collector_capture_incidentscreenshot", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L515", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_ensure_directory", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L603", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_ensure_screenshottypesloaded", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L604", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_get_incidentscreenshotpath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L611", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_get_filesha256hex", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L624", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L628", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_browserurlfromwindow", + "target": "browser_domains_native_collector_convertto_normalizedurl", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L678", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_categoryheartbeat", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1", + "source_location": "L753", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L16", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L24", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L36", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L46", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L91", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_filesha256hex", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L132", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_directory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L150", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L157", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L172", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L181", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L222", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L244", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L258", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L282", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_stringhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_load_dlppolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L324", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L366", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L384", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_usbrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L432", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_printrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L467", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L513", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L519", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_ownerloosematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L538", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_normalize_printerformatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L551", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_printerloosematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L564", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L577", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L597", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L647", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L676", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_ensure_bucket", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L63", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L74", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L88", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L102", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L107", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L129", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_ensure_directory", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L188", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L189", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L196", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_get_filesha256hex", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L209", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L213", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_show_enforcementnotification", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L240", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L249", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L267", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L300", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_load_dlppolicy", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L342", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L411", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L419", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L420", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L423", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L428", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L446", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L455", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L458", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L463", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L491", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L499", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L500", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L503", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L509", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_test_ownerloosematch", + "target": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L543", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_test_printerloosematch", + "target": "dlp_endpoint_signals_collector_normalize_printerformatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L569", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L605", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L662", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L692", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L693", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_ownerloosematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L695", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_printerloosematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L696", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L700", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L720", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L39", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_write_collectorlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L47", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L56", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L65", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_stringhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L81", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L92", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_send_emailincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L113", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_show_enforcementnotification", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L141", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_load_emailpolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L165", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_should_emitbycooldown", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L202", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L222", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_invoke_emailenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L302", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_initialize_outlookcom", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L324", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_outlooksentitems", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L338", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_poll_outlooksentitems", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L387", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_poll_smtpconnections", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L437", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_ensure_bucket", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L77", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailheartbeat", + "target": "email_outbound_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L98", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailheartbeat", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L110", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailincidentheartbeat", + "target": "email_outbound_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L122", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailincidentheartbeat", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L138", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_load_emailpolicy", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L181", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L274", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_invoke_emailenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L282", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L283", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_send_emailincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L289", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L290", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L298", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_invoke_emailenforcement", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_invoke_emailenforcement", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_initialize_outlookcom", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L329", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_get_outlooksentitems", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L382", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_get_outlooksentitems", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L388", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L395", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L397", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L409", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L411", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L458", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L466", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/email-outbound-collector.ps1", + "source_location": "L468", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_copy_directorycontents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L19", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_copy_ifexists", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L35", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_convert_pathvalue", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L48", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_stop_awatchtaskset", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L61", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_get_existingawatchconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L88", + "weight": 1.0 + }, + { + "source": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "target": "migrate_awatch_rus_paths_convert_pathvalue", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L111", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_get_config", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L20", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L30", + "weight": 1.0 + }, + { + "source": "install_kit_awindows_20260427_211240_windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_get_sessionrecords", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L53", + "weight": 1.0 + }, + { + "source": "worktime_session_collector_ensure_bucket", + "target": "worktime_session_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1", + "source_location": "L50", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "argparse", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L2", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "json", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L3", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "ssl", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L4", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "sys", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L5", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "time", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L6", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "urllib_error", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L7", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "urllib_parse", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L8", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "urllib_request", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L9", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "datetime", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pathlib", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L11", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_utc_now_iso", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L14", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_load_json", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L18", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_build_ssl_context", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L22", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_http_json", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L31", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L44", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_send_heartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L58", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_normalize_headers", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L68", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_summarize_payload", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L82", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_poll_endpoint", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L100", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_run_once", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L173", + "weight": 1.0 + }, + { + "source": "pfsense_pfsense_aw_poller_py", + "target": "pfsense_aw_poller_main", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L187", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_ensure_bucket", + "target": "pfsense_aw_poller_http_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L45", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_send_heartbeat", + "target": "pfsense_aw_poller_http_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L59", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_normalize_headers", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L106", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_http_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L109", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L118", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_utc_now_iso", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L128", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_summarize_payload", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L141", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_poll_endpoint", + "target": "pfsense_aw_poller_send_heartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L145", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_run_once", + "target": "pfsense_aw_poller_build_ssl_context", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L179", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_run_once", + "target": "pfsense_aw_poller_poll_endpoint", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L183", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_main", + "target": "pfsense_aw_poller_load_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L193", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_main", + "target": "pfsense_aw_poller_run_once", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L197", + "weight": 1.0 + }, + { + "source": "pfsense_aw_poller_main", + "target": "pfsense_aw_poller_utc_now_iso", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/pfsense/pfsense-aw-poller.py", + "source_location": "L198", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "argparse", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L2", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "json", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L3", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "os", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L4", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "sqlite3", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L5", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "sys", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L6", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "urllib_error", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L7", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "urllib_parse", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L8", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "urllib_request", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L9", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "collections_abc", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "dataclasses", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L11", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "datetime", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L12", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "pathlib", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L13", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "typing", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L14", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L49", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_awevent", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L57", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_psycopgconnection", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_psycopgconnection", + "target": "protocol", + "relation": "inherits", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_psycopgconnection", + "target": "aggregate_dlp_events_psycopgconnection_cursor", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L68", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_psycopgconnection", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "method", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L71", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_utc_now", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L75", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_parse_timestamp", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L79", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_format_aw_timestamp", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L87", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_load_state", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L91", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_save_state", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L97", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_normalize_base_url", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L102", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_aw_get_json", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L106", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_list_buckets", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L113", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_bucket_stream_type", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L132", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_select_buckets", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L140", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_build_events_path", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L149", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_event_key", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L160", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_fetch_bucket_events", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L165", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_connect_sqlite", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L201", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_ensure_schema", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L210", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_ensure_postgres_schema", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L258", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_first_string", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L307", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_bool_as_int", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L315", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_event_row", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L323", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_insert_events", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L351", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_insert_postgres_events", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L389", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_get_start_time", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L416", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_parse_prefixes", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L424", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_build_parser", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L431", + "weight": 1.0 + }, + { + "source": "scripts_aggregate_dlp_events_py", + "target": "aggregate_dlp_events_main", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L447", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_aw_get_json", + "target": "aggregate_dlp_events_normalize_base_url", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L107", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_list_buckets", + "target": "aggregate_dlp_events_aw_get_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L114", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_list_buckets", + "target": "aggregate_dlp_events_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L122", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_select_buckets", + "target": "aggregate_dlp_events_bucket_stream_type", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L143", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_build_events_path", + "target": "aggregate_dlp_events_format_aw_timestamp", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L152", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_fetch_bucket_events", + "target": "aggregate_dlp_events_aw_get_json", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L174", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_fetch_bucket_events", + "target": "aggregate_dlp_events_build_events_path", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L174", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_fetch_bucket_events", + "target": "aggregate_dlp_events_event_key", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L186", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_fetch_bucket_events", + "target": "aggregate_dlp_events_awevent", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L188", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_ensure_schema", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L255", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_ensure_postgres_schema", + "target": "aggregate_dlp_events_psycopgconnection_cursor", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L259", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_ensure_postgres_schema", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L304", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_event_row", + "target": "aggregate_dlp_events_event_key", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L325", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_event_row", + "target": "aggregate_dlp_events_first_string", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L331", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_event_row", + "target": "aggregate_dlp_events_bool_as_int", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L338", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_events", + "target": "aggregate_dlp_events_format_aw_timestamp", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L353", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_events", + "target": "aggregate_dlp_events_utc_now", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L353", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_events", + "target": "aggregate_dlp_events_event_row", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L382", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_events", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L385", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_format_aw_timestamp", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L396", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_utc_now", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L396", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_ensure_postgres_schema", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L405", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_psycopgconnection_cursor", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L406", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_event_row", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L408", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L412", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_get_start_time", + "target": "aggregate_dlp_events_parse_timestamp", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L418", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_get_start_time", + "target": "aggregate_dlp_events_utc_now", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L421", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_build_parser", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L448", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_load_state", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L450", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_get_start_time", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L451", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_utc_now", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L452", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_select_buckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_list_buckets", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_fetch_bucket_events", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L457", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_format_aw_timestamp", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L464", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_insert_postgres_events", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L478", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_connect_sqlite", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L483", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_ensure_schema", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L485", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_insert_events", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L486", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_main", + "target": "aggregate_dlp_events_save_state", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L491", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "argparse", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L2", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "json", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L3", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "os", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L4", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "shutil", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L5", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "sqlite3", + "relation": "imports", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L6", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "pathlib", + "relation": "imports_from", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L7", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "merge_aw_server_dbs_connect", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "merge_aw_server_dbs_bucket_key", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L17", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "merge_aw_server_dbs_ensure_parent", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L26", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "merge_aw_server_dbs_load_existing_events", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L30", + "weight": 1.0 + }, + { + "source": "scripts_merge_aw_server_dbs_py", + "target": "merge_aw_server_dbs_main", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L38", + "weight": 1.0 + }, + { + "source": "merge_aw_server_dbs_main", + "target": "merge_aw_server_dbs_ensure_parent", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L52", + "weight": 1.0 + }, + { + "source": "merge_aw_server_dbs_main", + "target": "merge_aw_server_dbs_connect", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L58", + "weight": 1.0 + }, + { + "source": "merge_aw_server_dbs_main", + "target": "merge_aw_server_dbs_bucket_key", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L73", + "weight": 1.0 + }, + { + "source": "merge_aw_server_dbs_main", + "target": "merge_aw_server_dbs_load_existing_events", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L102", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L42", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L109", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_write_dlpincidentlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L123", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L137", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_hostfromurl", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L152", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_rootdomain", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L173", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_convertto_normalizedurl", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L194", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_load_customcategoryrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L225", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_webcategory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L274", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_domainlistmatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L296", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_dlpruletimewindow", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L315", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_load_dlppolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L337", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_test_dlprulematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L398", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_dlpdecision", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_should_emitincident", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L478", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_dlpincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L496", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_filesha256hex", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L547", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_directory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L565", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_incidentscreenshotpath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L572", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_screenshottypesloaded", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L587", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_capture_incidentscreenshot", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L596", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_foregroundwindowcontext", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L633", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_get_browserurlfromwindow", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L661", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L693", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_heartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L714", + "weight": 1.0 + }, + { + "source": "windows_browser_domains_native_collector_ps1", + "target": "browser_domains_native_collector_send_categoryheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L739", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_load_customcategoryrules", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L266", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_webcategory", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L279", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_domainlistmatch", + "target": "browser_domains_native_collector_test_domainmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L307", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_load_dlppolicy", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L341", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_dlprulematch", + "target": "browser_domains_native_collector_test_dlpruletimewindow", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L416", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_test_dlprulematch", + "target": "browser_domains_native_collector_test_domainlistmatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L421", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_dlpdecision", + "target": "browser_domains_native_collector_test_dlprulematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L470", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_dlpincidentheartbeat", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L510", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_dlpincidentheartbeat", + "target": "browser_domains_native_collector_capture_incidentscreenshot", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L515", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_ensure_directory", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L603", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_ensure_screenshottypesloaded", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L604", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_get_incidentscreenshotpath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L611", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_get_filesha256hex", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L624", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_capture_incidentscreenshot", + "target": "browser_domains_native_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L628", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_get_browserurlfromwindow", + "target": "browser_domains_native_collector_convertto_normalizedurl", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L678", + "weight": 1.0 + }, + { + "source": "browser_domains_native_collector_send_categoryheartbeat", + "target": "browser_domains_native_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1", + "source_location": "L753", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L16", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L24", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L36", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L46", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L91", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_filesha256hex", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L132", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_directory", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L150", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L157", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L172", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L181", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L222", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L244", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L258", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L282", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_stringhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_load_dlppolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L324", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L366", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L384", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_usbrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L432", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_evaluate_printrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L467", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L513", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L519", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_ownerloosematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L538", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_normalize_printerformatch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L551", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_test_printerloosematch", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L564", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L577", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L597", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L647", + "weight": 1.0 + }, + { + "source": "windows_dlp_endpoint_signals_collector_ps1", + "target": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L676", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_ensure_bucket", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L63", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L74", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_endpointsignalheartbeat", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L88", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L102", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L107", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "target": "dlp_endpoint_signals_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L129", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_ensure_directory", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L188", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_ensure_screenshottypesloaded", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L189", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_get_incidentscreenshotpath", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L196", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_get_filesha256hex", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L209", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_capture_incidentscreenshot", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L213", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_show_enforcementnotification", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L240", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L249", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L267", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L300", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_load_dlppolicy", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L342", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L411", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_invoke_clipboardenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L419", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L420", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L423", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_clipboardrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L428", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L446", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_invoke_usbwriteblockenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L454", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L455", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L458", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_usbrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L463", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L491", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_invoke_printjobenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L499", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L500", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_send_dlpincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L503", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_evaluate_printrules", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L509", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_test_ownerloosematch", + "target": "dlp_endpoint_signals_collector_normalize_ownerformatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L543", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_test_printerloosematch", + "target": "dlp_endpoint_signals_collector_normalize_printerformatch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L569", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L605", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "target": "dlp_endpoint_signals_collector_write_endpointlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L662", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_get_printserviceeventsummary", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L692", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_get_printservicedocumentfallback", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L693", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_ownerloosematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L695", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_printerloosematch", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L696", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_write_printserviceeventtrace", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L700", + "weight": 1.0 + }, + { + "source": "dlp_endpoint_signals_collector_get_betterdocumentnamefromprintserviceevents", + "target": "dlp_endpoint_signals_collector_test_lookslikemojibakequestionmarks", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/dlp-endpoint-signals-collector.ps1", + "source_location": "L720", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L39", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_write_collectorlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L47", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L56", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L65", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_stringhash", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L81", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L92", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_send_emailincidentheartbeat", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L113", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_show_enforcementnotification", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L141", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_load_emailpolicy", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L165", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_should_emitbycooldown", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L202", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L222", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_invoke_emailenforcement", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L302", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_initialize_outlookcom", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L324", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_get_outlooksentitems", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L338", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_poll_outlooksentitems", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L387", + "weight": 1.0 + }, + { + "source": "windows_email_outbound_collector_ps1", + "target": "email_outbound_collector_poll_smtpconnections", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L437", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_ensure_bucket", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L77", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailheartbeat", + "target": "email_outbound_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L98", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailheartbeat", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L110", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailincidentheartbeat", + "target": "email_outbound_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L122", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_send_emailincidentheartbeat", + "target": "email_outbound_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L138", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_load_emailpolicy", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L181", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_should_emitbycooldown", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L274", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_invoke_emailenforcement", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L282", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_show_enforcementnotification", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L283", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_send_emailincidentheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L289", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L290", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_evaluate_emailrules", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L298", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_invoke_emailenforcement", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_invoke_emailenforcement", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L311", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_initialize_outlookcom", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L329", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_get_outlooksentitems", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L382", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_get_outlooksentitems", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L388", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_get_stringhash", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L395", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L397", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L409", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_outlooksentitems", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L411", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_send_emailheartbeat", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L458", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_write_collectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L466", + "weight": 1.0 + }, + { + "source": "email_outbound_collector_poll_smtpconnections", + "target": "email_outbound_collector_evaluate_emailrules", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/email-outbound-collector.ps1", + "source_location": "L468", + "weight": 1.0 + }, + { + "source": "windows_file_operations_collector_ps1", + "target": "file_operations_collector_get_deploymentconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L30", + "weight": 1.0 + }, + { + "source": "windows_file_operations_collector_ps1", + "target": "file_operations_collector_write_filecollectorlog", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L38", + "weight": 1.0 + }, + { + "source": "windows_file_operations_collector_ps1", + "target": "file_operations_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L46", + "weight": 1.0 + }, + { + "source": "windows_file_operations_collector_ps1", + "target": "file_operations_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L61", + "weight": 1.0 + }, + { + "source": "windows_file_operations_collector_ps1", + "target": "file_operations_collector_send_fileoperationevent", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L77", + "weight": 1.0 + }, + { + "source": "file_operations_collector_invoke_awjsonpost", + "target": "file_operations_collector_write_filecollectorlog", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L57", + "weight": 1.0 + }, + { + "source": "file_operations_collector_ensure_bucket", + "target": "file_operations_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L73", + "weight": 1.0 + }, + { + "source": "file_operations_collector_send_fileoperationevent", + "target": "file_operations_collector_ensure_bucket", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L86", + "weight": 1.0 + }, + { + "source": "file_operations_collector_send_fileoperationevent", + "target": "file_operations_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/file-operations-collector.ps1", + "source_location": "L109", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_copy_directorycontents", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L19", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_copy_ifexists", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L35", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_convert_pathvalue", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L48", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_stop_awatchtaskset", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L61", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_get_existingawatchconfig", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L67", + "weight": 1.0 + }, + { + "source": "windows_migrate_awatch_rus_paths_ps1", + "target": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L88", + "weight": 1.0 + }, + { + "source": "migrate_awatch_rus_paths_update_awatchconfigpaths", + "target": "migrate_awatch_rus_paths_convert_pathvalue", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/migrate-awatch-rus-paths.ps1", + "source_location": "L111", + "weight": 1.0 + }, + { + "source": "windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_get_config", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L10", + "weight": 1.0 + }, + { + "source": "windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_invoke_awjsonpost", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L20", + "weight": 1.0 + }, + { + "source": "windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_ensure_bucket", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L30", + "weight": 1.0 + }, + { + "source": "windows_worktime_session_collector_ps1", + "target": "worktime_session_collector_get_sessionrecords", + "relation": "contains", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L53", + "weight": 1.0 + }, + { + "source": "worktime_session_collector_ensure_bucket", + "target": "worktime_session_collector_invoke_awjsonpost", + "relation": "calls", + "confidence": "EXTRACTED", + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/worktime-session-collector.ps1", + "source_location": "L50", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_connect_sqlite", + "target": "merge_aw_server_dbs_connect", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L203", + "weight": 1.0 + }, + { + "source": "aggregate_dlp_events_insert_postgres_events", + "target": "merge_aw_server_dbs_connect", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/aggregate_dlp_events.py", + "source_location": "L404", + "weight": 1.0 + }, + { + "source": "merge_aw_server_dbs_main", + "target": "aggregate_dlp_events_psycopgconnection_commit", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "/mnt/usb_hdd2/Projects/ActivityWatch-Russian/scripts/merge_aw_server_dbs.py", + "source_location": "L117", + "weight": 1.0 + } + ], + "metadata": { + "source": "AST only", + "total_files": 29, + "ast_nodes": 404, + "ast_edges": 933, + "communities": 27 + } +} \ No newline at end of file diff --git a/install-kit-awindows-20260427-211240/ansible/README.md b/install-kit-awindows-20260427-211240/ansible/README.md index 3c9d42a..b22fc6c 100644 --- a/install-kit-awindows-20260427-211240/ansible/README.md +++ b/install-kit-awindows-20260427-211240/ansible/README.md @@ -31,15 +31,6 @@ cd ansible ansible-playbook -i inventory.ini deploy_aw_server.yml ``` -## Секреты (пароли) безопасно - -Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения: - -- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD` -- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD` - -В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`. - ## Полный установочный playbook (всё за один запуск) Если нужно прогнать полный цикл одной командой: @@ -158,13 +149,3 @@ Playbook: - Для полного сценария CT создаётся автоматически через `pct create`. - На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector. - Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`. - -## Prod rollout одной командой - -Для ручного запуска с dry-run и логированием используйте: - -```bash -bash scripts/prod_rollout.sh -``` - -Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`. diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml index 996eb4b..b136df7 100644 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml @@ -54,11 +54,6 @@ - "{{ aw_server_webui_dir }}" - "{{ aw_server_webui_dir }}/js" - "{{ aw_server_data_dir }}" - - "{{ aw_server_db_path | dirname }}" - - "{{ aw_server_data_dir }}/.config" - - "{{ aw_server_data_dir }}/.config/activitywatch" - - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" - - "{{ aw_server_data_dir }}/backups" - "{{ aw_server_log_dir }}" - /etc/activitywatch - "{{ aw_bootstrap_dir }}" @@ -79,147 +74,100 @@ - "{{ aw_server_webui_dir }}" - "{{ aw_server_webui_dir }}/js" - "{{ aw_server_data_dir }}" - - "{{ aw_server_db_path | dirname }}" - - "{{ aw_server_data_dir }}/.config" - - "{{ aw_server_data_dir }}/.config/activitywatch" - - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" - - "{{ aw_server_data_dir }}/backups" - "{{ aw_server_log_dir }}" - - name: (Check mode) Пропустить установку релиза ActivityWatch - ansible.builtin.debug: - msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped." - when: ansible_check_mode + - name: Скачать архив релиза ActivityWatch + ansible.builtin.get_url: + url: "{{ aw_server_download_url }}" + dest: "{{ aw_archive_path }}" + mode: "0644" - - name: Установить релиз ActivityWatch (download/unarchive/install) - when: not ansible_check_mode - block: - - name: Скачать архив релиза ActivityWatch - ansible.builtin.get_url: - url: "{{ aw_server_download_url }}" - dest: "{{ aw_archive_path }}" - mode: "0644" + - name: Распаковать релиз ActivityWatch + ansible.builtin.unarchive: + src: "{{ aw_archive_path }}" + dest: "{{ aw_release_dir }}" + remote_src: true + extra_opts: ["-o"] - - name: Распаковать релиз ActivityWatch - ansible.builtin.unarchive: - src: "{{ aw_archive_path }}" - dest: "{{ aw_release_dir }}" - remote_src: true - extra_opts: ["-o"] + - name: Найти распакованный каталог ActivityWatch + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + file_type: directory + patterns: "activitywatch*" + register: aw_release_find - - name: Найти распакованный каталог ActivityWatch - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - recurse: true - file_type: directory - patterns: "activitywatch*" - register: aw_release_find + - name: Найти бинарный файл AW server + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + file_type: file + patterns: + - aw-server-rust + - aw-server + register: aw_server_binary_find - - name: Найти бинарный файл AW server - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - recurse: true - file_type: file - patterns: - - aw-server-rust - - aw-server - register: aw_server_binary_find + - name: Найти каталог WebUI + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + file_type: directory + patterns: + - aw-webui + - webui + register: aw_webui_dir_find - - name: Найти index.html WebUI - ansible.builtin.find: - paths: "{{ aw_release_dir }}" - recurse: true - file_type: file - patterns: - - index.html - register: aw_webui_index_find + - name: Сохранить пути распакованного релиза + ansible.builtin.set_fact: + aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" + aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" + aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" - - name: Сохранить пути распакованного релиза (binary + webui index) - ansible.builtin.set_fact: - aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" - aw_server_binary_path: >- - {{ - ( - ( - (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) - | select('match', '.*/aw-server-rust$') | list | first - ) - | default( - ( - (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) - ), - true - ) - ) | default('') - }} - aw_webui_index_path: >- - {{ - ( - ( - (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) - | select('search', '/static/index\\.html$') | list | first - ) - | default( - ( - (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) - ), - true - ) - ) | default('') - }} + - name: Проверить, что компоненты релиза найдены + ansible.builtin.assert: + that: + - aw_release_extracted is defined + - aw_release_extracted | length > 0 + - aw_server_binary_path is defined + - aw_server_binary_path | length > 0 + - aw_webui_source_path is defined + - aw_webui_source_path | length > 0 + fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch." - - name: Сохранить каталог WebUI (dirname index.html) - ansible.builtin.set_fact: - aw_webui_source_path: "{{ aw_webui_index_path | dirname }}" + - name: Создать каталог установленного релиза + ansible.builtin.file: + path: "{{ aw_release_install_dir }}" + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" - - name: Проверить, что компоненты релиза найдены - ansible.builtin.assert: - that: - - aw_release_extracted is defined - - aw_release_extracted | length > 0 - - aw_server_binary_path is defined - - aw_server_binary_path | length > 0 - - aw_webui_source_path is defined - - aw_webui_source_path | length > 0 - fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch." + - name: Установить бинарный файл AW server + ansible.builtin.copy: + remote_src: true + src: "{{ aw_server_binary_path }}" + dest: "{{ aw_release_install_dir }}/aw-server-rust" + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" - - name: Создать каталог установленного релиза - ansible.builtin.file: - path: "{{ aw_release_install_dir }}" - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" + - name: Создать ссылку на активный бинарный файл AW server + ansible.builtin.file: + src: "{{ aw_release_install_dir }}/aw-server-rust" + dest: /opt/activitywatch/bin/aw-server-rust + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + state: link + force: true - - name: Установить бинарный файл AW server - ansible.builtin.copy: - remote_src: true - src: "{{ aw_server_binary_path }}" - dest: "{{ aw_release_install_dir }}/aw-server-rust" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" + - name: Синхронизировать WebUI в RU каталог + ansible.builtin.command: + cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" - - name: Создать ссылку на активный бинарный файл AW server - ansible.builtin.file: - src: "{{ aw_release_install_dir }}/aw-server-rust" - dest: /opt/activitywatch/bin/aw-server-rust - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - state: link - force: true - - - name: Синхронизировать WebUI в RU каталог - ansible.builtin.command: - cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" - - - name: Настроить владельца файлов /opt/activitywatch - ansible.builtin.file: - path: /opt/activitywatch - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - recurse: true + - name: Настроить владельца файлов /opt/activitywatch + ansible.builtin.file: + path: /opt/activitywatch + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + recurse: true - name: Установить systemd service из шаблона репозитория ansible.builtin.copy: @@ -236,401 +184,143 @@ - Перезагрузить systemd - Перезапустить activitywatch - - name: (Check mode) Пропустить WebUI patch и запуск сервиса - ansible.builtin.debug: - msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped." - when: ansible_check_mode - - - name: Применить WebUI RU patch и запустить сервис - when: not ansible_check_mode - block: - - name: Скопировать RU patch файлы WebUI из репозитория - ansible.builtin.copy: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - mode: "{{ item.mode }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - loop: - - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } - - - name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh - ansible.builtin.file: - path: /root/bootstrap - state: directory - mode: "0755" - - - name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков) - ansible.builtin.copy: - src: "{{ item.src }}" - dest: "{{ item.dest }}" - mode: "{{ item.mode }}" - loop: - - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" } - - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - - - name: Скопировать apply_webui_ru_patch.sh скрипт - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh" - dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh - mode: "0755" - - - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами - ansible.builtin.copy: - dest: /etc/activitywatch/aw-server.env - mode: "0640" - owner: root - group: root - content: | - AW_SERVER_BIND_HOST={{ aw_server_bind_host }} - AW_SERVER_PORT={{ aw_server_port }} - AW_SERVER_DATA_DIR={{ aw_server_data_dir }} - AW_SERVER_DB_PATH={{ aw_server_db_path }} - AW_SERVER_LOG_DIR={{ aw_server_log_dir }} - AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} - AW_SERVER_USER={{ aw_server_user }} - AW_SERVER_GROUP={{ aw_server_group }} - AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }} - AW_WORKTIME_TZ={{ aw_worktime_timezone }} - XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share - XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config - - - name: Установить скрипт AW worktime API - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py" - dest: /usr/local/bin/aw-worktime-api.py - owner: root - group: root - mode: "0755" - - - name: Установить systemd unit AW worktime API - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service" - dest: /etc/systemd/system/aw-worktime-api.service - owner: root - group: root - mode: "0644" - - - name: Установить скрипт AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.py" - dest: /usr/local/bin/aw-worktime-ui-bridge.py - owner: root - group: root - mode: "0755" - - - name: Установить systemd unit AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.service" - dest: /etc/systemd/system/aw-worktime-ui-bridge.service - owner: root - group: root - mode: "0644" - - - name: Установить systemd timer AW worktime UI bridge - ansible.builtin.copy: - src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.timer" - dest: /etc/systemd/system/aw-worktime-ui-bridge.timer - owner: root - group: root - mode: "0644" - - - name: Перезагрузить systemd после установки AW worktime API - ansible.builtin.systemd: - daemon_reload: true - - - name: Включить и перезапустить AW worktime API - ansible.builtin.systemd: - name: aw-worktime-api.service - enabled: true - state: restarted - - - name: Отключить legacy timer aw-worktime-afk-bridge (если есть) - ansible.builtin.systemd: - name: aw-worktime-afk-bridge.timer - enabled: false - state: stopped - failed_when: false - - - name: Включить и перезапустить AW worktime UI bridge timer - ansible.builtin.systemd: - name: aw-worktime-ui-bridge.timer - enabled: true - state: restarted - - - name: Выполнить разовый прогон AW worktime UI bridge - ansible.builtin.systemd: - name: aw-worktime-ui-bridge.service - state: started - failed_when: false - - - name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper) - ansible.builtin.command: - cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh" - register: apply_ru_patch_result - failed_when: false - - - name: Вывести результат применения хотфиксов - ansible.builtin.debug: - msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}" - - - name: Проверить наличие index.html после копирования - ansible.builtin.stat: - path: "{{ aw_server_webui_dir }}/index.html" - register: aw_webui_ru_index - - - name: Проверить, что index.html доступен для RU patch - ansible.builtin.assert: - that: - - aw_webui_ru_index.stat.exists - fail_msg: "Не найден index.html WebUI для применения RU patch." - - - name: Удалить старые теги RU patch из index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' - replace: '' - - - name: Добавить cleanup script RU patch в index.html - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' - - - name: Добавить загрузчик RU patch перед закрытием body - ansible.builtin.replace: - path: "{{ aw_server_webui_dir }}/index.html" - regexp: '' - replace: '' - - - name: Скопировать merge script AW DB на сервер - ansible.builtin.copy: - src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py" - dest: /usr/local/bin/merge_aw_server_dbs.py - owner: root - group: root - mode: "0755" - - - name: Проверить наличие legacy root DB - ansible.builtin.stat: - path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db - register: aw_legacy_root_db - - - name: Проверить наличие target DB - ansible.builtin.stat: - path: "{{ aw_server_db_path }}" - register: aw_target_db - - - name: Остановить сервис перед merge server DB - ansible.builtin.systemd: - name: activitywatch-server.service - state: stopped - when: aw_legacy_root_db.stat.exists | default(false) - - - name: Создать backup каталоги server DB - ansible.builtin.file: - path: "{{ aw_server_data_dir }}/backups/db" - state: directory - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0755" - when: aw_legacy_root_db.stat.exists | default(false) - - - name: Backup target DB перед merge - ansible.builtin.copy: - remote_src: true - src: "{{ aw_server_db_path }}" - dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - when: - - aw_legacy_root_db.stat.exists | default(false) - - aw_target_db.stat.exists | default(false) - - - name: Backup legacy root DB перед merge - ansible.builtin.copy: - remote_src: true - src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db - dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - when: aw_legacy_root_db.stat.exists | default(false) - - - name: Merge legacy root DB в target DB - ansible.builtin.command: - argv: - - python3 - - /usr/local/bin/merge_aw_server_dbs.py - - --base - - /root/.local/share/activitywatch/aw-server-rust/sqlite.db - - --overlay - - "{{ aw_server_db_path }}" - - --output - - "{{ aw_server_db_path }}.merged" - when: - - aw_legacy_root_db.stat.exists | default(false) - - aw_target_db.stat.exists | default(false) - - - name: Install merged DB as active target DB - ansible.builtin.copy: - remote_src: true - src: "{{ aw_server_db_path }}.merged" - dest: "{{ aw_server_db_path }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - when: - - aw_legacy_root_db.stat.exists | default(false) - - aw_target_db.stat.exists | default(false) - - - name: Скопировать legacy root DB в target DB если target ещё не существует - ansible.builtin.copy: - remote_src: true - src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db - dest: "{{ aw_server_db_path }}" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - when: - - aw_legacy_root_db.stat.exists | default(false) - - not (aw_target_db.stat.exists | default(false)) - - - name: Записать aw-server-rust config.toml с разрешёнными CORS origin - ansible.builtin.copy: - dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml" - owner: "{{ aw_server_user }}" - group: "{{ aw_server_group }}" - mode: "0644" - content: | - cors = [ - {% for origin in aw_server_cors_origins | default([]) %} - "{{ origin }}"{% if not loop.last %},{% endif %} - {% endfor %} - ] - - - name: Включить и запустить сервис - ansible.builtin.systemd: - name: activitywatch-server.service - enabled: true - state: restarted - daemon_reload: true - - - name: Дождаться ответа API - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" - method: GET - status_code: 200 - register: aw_api - retries: 10 - delay: 3 - until: aw_api.status == 200 - - - name: Считать текущие server-side settings - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/" - method: GET - status_code: 200 - register: aw_settings_current - when: aw_apply_worktime_settings | default(false) | bool - - - name: Считать текущие server-side views - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views" - method: GET - status_code: 200 - register: aw_views_current - when: aw_apply_worktime_settings | default(false) | bool - - - name: Считать текущие server-side classes - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes" - method: GET - status_code: 200 - register: aw_classes_current - when: aw_apply_worktime_settings | default(false) | bool - - - name: Создать backup текущих server-side settings/views/classes + - name: Скопировать RU patch файлы WebUI из репозитория ansible.builtin.copy: - dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json" + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "{{ item.mode }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" - mode: "0644" - content: "{{ item.payload | to_nice_json }}" loop: - - name: settings - payload: "{{ aw_settings_current.json | default({}) }}" - - name: views - payload: "{{ aw_views_current.json | default(none) }}" - - name: classes - payload: "{{ aw_classes_current.json | default(none) }}" - when: aw_apply_worktime_settings | default(false) | bool + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } - - name: Настроить DLP Aggregator (Phase 2) - block: - - name: Создать каталог для скриптов - ansible.builtin.file: - path: "/opt/activitywatch/scripts" - state: directory - owner: root - group: root - mode: "0755" + - name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh + ansible.builtin.file: + path: /root/bootstrap + state: directory + mode: "0755" - - name: Скопировать агрегатор событий DLP - ansible.builtin.copy: - src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py" - dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py" - owner: root - group: root - mode: "0755" + - name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков) + ansible.builtin.copy: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "{{ item.mode }}" + loop: + - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" } + - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - - name: Установить systemd unit для агрегатора - ansible.builtin.copy: - dest: /etc/systemd/system/activitywatch-dlp-aggregator.service - content: | - [Unit] - Description=ActivityWatch DLP Event Aggregator - After=activitywatch-server.service + - name: Скопировать apply_webui_ru_patch.sh скрипт + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh" + dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh + mode: "0755" - [Service] - Type=oneshot - User={{ aw_server_user }} - WorkingDirectory={{ aw_server_data_dir }} - ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \ - --aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \ - --sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \ - --state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json + - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами + ansible.builtin.copy: + dest: /etc/activitywatch/aw-server.env + mode: "0640" + owner: root + group: root + content: | + AW_SERVER_BIND_HOST={{ aw_server_bind_host }} + AW_SERVER_PORT={{ aw_server_port }} + AW_SERVER_DATA_DIR={{ aw_server_data_dir }} + AW_SERVER_DB_PATH={{ aw_server_db_path }} + AW_SERVER_LOG_DIR={{ aw_server_log_dir }} + AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} + AW_SERVER_USER={{ aw_server_user }} + AW_SERVER_GROUP={{ aw_server_group }} + AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }} + AW_WORKTIME_TZ={{ aw_worktime_timezone }} + XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share + XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config - [Install] - WantedBy=multi-user.target + - name: Установить скрипт AW worktime API + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py" + dest: /usr/local/bin/aw-worktime-api.py + owner: root + group: root + mode: "0755" - - name: Установить systemd timer для агрегатора - ansible.builtin.copy: - dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer - content: | - [Unit] - Description=Run ActivityWatch DLP Aggregator every 5 minutes + - name: Установить systemd unit AW worktime API + ansible.builtin.copy: + src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service" + dest: /etc/systemd/system/aw-worktime-api.service + owner: root + group: root + mode: "0644" - [Timer] - OnBootSec=1min - OnUnitActiveSec=5min - AccuracySec=1s + - name: Перезагрузить systemd после установки AW worktime API + ansible.builtin.systemd: + daemon_reload: true - [Install] - WantedBy=timers.target + - name: Включить и перезапустить AW worktime API + ansible.builtin.systemd: + name: aw-worktime-api.service + enabled: true + state: restarted - - name: Включить и запустить таймер агрегатора - ansible.builtin.systemd: - name: activitywatch-dlp-aggregator.timer - enabled: true - state: started - daemon_reload: true + - name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper) + ansible.builtin.command: + cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh" + register: apply_ru_patch_result + failed_when: false + + - name: Вывести результат применения хотфиксов + ansible.builtin.debug: + msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}" + + - name: Проверить наличие index.html после копирования + ansible.builtin.stat: + path: "{{ aw_server_webui_dir }}/index.html" + register: aw_webui_ru_index + + - name: Проверить, что index.html доступен для RU patch + ansible.builtin.assert: + that: + - aw_webui_ru_index.stat.exists + fail_msg: "Не найден index.html WebUI для применения RU patch." + + - name: Удалить старые теги RU patch из index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' + replace: '' + + - name: Добавить cleanup script RU patch в index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - name: Добавить загрузчик RU patch перед закрытием body + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - name: Включить и запустить сервис + ansible.builtin.systemd: + name: activitywatch-server.service + enabled: true + state: restarted + daemon_reload: true + + - name: Дождаться ответа API + ansible.builtin.uri: + url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" + method: GET + status_code: 200 + register: aw_api + retries: 10 + delay: 3 + until: aw_api.status == 200 - name: Применить базовые worktime settings (classes) ansible.builtin.uri: @@ -652,12 +342,16 @@ - name: Вычислить worktime durationDefault из aw_worktime_from/to ansible.builtin.set_fact: + aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}" + aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}" + aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}" + aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}" aw_worktime_duration_default_derived: >- {{ ( ( - (((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) - - (((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int)) + ((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) - + ((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int)) ) * 60 ) }} @@ -685,46 +379,20 @@ ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay" method: POST - body: "\"{{ aw_worktime_start_of_day }}\"" - headers: - Content-Type: application/json - status_code: [200, 201] + body: "{{ aw_worktime_start_of_day }}" + body_format: json + status_code: 200 when: aw_apply_worktime_settings | default(false) | bool - name: Применить базовый период worktime (durationDefault seconds) ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault" method: POST - body: "{{ aw_worktime_duration_default_effective | string }}" - headers: - Content-Type: application/json - status_code: [200, 201] + body: "{{ aw_worktime_duration_default_effective }}" + body_format: json + status_code: 200 when: aw_apply_worktime_settings | default(false) | bool - - name: Применить always_active_pattern для fallback без AFK - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern" - method: POST - body: "\"{{ aw_server_always_active_pattern }}\"" - headers: - Content-Type: application/json - status_code: [200, 201] - when: - - aw_apply_worktime_settings | default(false) | bool - - (aw_server_always_active_pattern | default('') | string | length) > 0 - - - name: Применить landingpage профиля - ansible.builtin.uri: - url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage" - method: POST - body: "\"{{ aw_server_landingpage }}\"" - headers: - Content-Type: application/json - status_code: [200, 201] - when: - - aw_apply_worktime_settings | default(false) | bool - - (aw_server_landingpage | default('') | string | length) > 0 - handlers: - name: Перезагрузить systemd ansible.builtin.systemd: diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml index d7384ff..c78830c 100644 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml @@ -25,7 +25,6 @@ aw_windows_state_root: "C:\\ProgramData\\AWatch-rus" aw_windows_afk_enabled: true aw_windows_window_enabled: true - aw_windows_file_ops_enabled: true aw_windows_local_agent_logs_enabled: false aw_windows_incident_capture_enabled: true aw_windows_incident_screenshot_enabled: true @@ -79,7 +78,6 @@ - browser-domains-native-collector.ps1 - dlp-endpoint-signals-collector.ps1 - email-outbound-collector.ps1 - - file-operations-collector.ps1 - worktime-session-collector.ps1 - migrate-awatch-rus-paths.ps1 - deploy-domain-users.ps1 @@ -89,18 +87,6 @@ - web-category-rules.example.json - dlp-policy.example.json - - name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell) - ansible.windows.win_powershell: - script: | - $ErrorActionPreference = 'Stop' - $toolkitDir = "{{ aw_windows_deploy_root }}\windows" - $encIn = New-Object System.Text.UTF8Encoding($false) - $encOut = New-Object System.Text.UTF8Encoding($true) - Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object { - $text = [System.IO.File]::ReadAllText($_.FullName, $encIn) - [System.IO.File]::WriteAllText($_.FullName, $text, $encOut) - } - - name: Загрузить список пользователей для доменного развёртывания ansible.windows.win_copy: dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt" @@ -145,7 +131,6 @@ StateRoot = "{{ aw_windows_state_root }}" AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }} WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }} - FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }} LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }} IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }} IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }} @@ -165,19 +150,6 @@ {% endif %} & "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params - - name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config - ansible.windows.win_powershell: - script: | - $ErrorActionPreference = 'Stop' - $config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json - $desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName }) - foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) { - if ($desired -notcontains [string]$task.TaskName) { - Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue - & cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null - } - } - - name: Принудительно запустить ActivityWatch recovery и launch tasks when: aw_windows_force_task_restart | bool ansible.windows.win_powershell: @@ -200,7 +172,6 @@ when: - aw_windows_api_smoke_check_enabled | bool - aw_windows_afk_enabled | bool - - aw_windows_hostname_result.stdout is defined ansible.builtin.set_fact: aw_windows_api_smoke_check_bucket_effective: >- {{ @@ -209,51 +180,54 @@ else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim) }} - - name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете) + - name: Дождаться свежих AFK событий на AW server when: - aw_windows_api_smoke_check_enabled | bool - aw_windows_afk_enabled | bool + delegate_to: localhost ansible.builtin.uri: url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}" method: GET - status_code: 200 - register: aw_windows_api_smoke_result - until: aw_windows_api_smoke_result.json | length > 0 - retries: 5 - delay: 5 - ignore_errors: true + return_content: true + register: aw_windows_api_smoke + until: > + aw_windows_api_smoke.status == 200 and + (aw_windows_api_smoke.json | length) > 0 and + ( + aw_windows_api_smoke.json + | selectattr('data.status', 'equalto', 'not-afk') + | list + | length + ) > 0 + retries: 10 + delay: 6 - - name: Валидировать развёртывание на эндпоинте + - name: Выполнить валидацию и сохранить отчёт на целевом Windows host ansible.windows.win_powershell: script: | $ErrorActionPreference = 'Stop' - $result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" ` + $report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" ` -ConfigPath "{{ aw_windows_state_root }}\deployment-config.json" - $result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8 - return $result + $report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8 + if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) { + throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}" + } - - name: Создать локальную директорию для отчётов валидации + - name: Создать локальный каталог для validation reports ansible.builtin.file: path: "{{ aw_windows_validation_local_dir }}" state: directory mode: "0755" delegate_to: localhost - - name: Стянуть отчёт валидации с эндпоинта + - name: Забрать validation report ansible.builtin.fetch: src: "{{ aw_windows_validation_remote_path }}" dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json" flat: true - - name: Проверить статус валидации - ansible.builtin.shell: | - python3 - <<'PY' - import json, sys - with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f: - data = json.load(f) - if not data.get('overallOk', False): - print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}") - sys.exit(1) - PY - delegate_to: localhost - when: aw_windows_fail_on_validation_error | bool + - name: Показать путь к отчёту + ansible.builtin.debug: + msg: + - "Windows/RDP развёртывание завершено на {{ inventory_hostname }}." + - "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml index 171b218..be28ad3 100644 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml +++ b/install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml @@ -4,7 +4,6 @@ aw_server_bind_host: "0.0.0.0" aw_server_port: 5600 aw_server_webui_dir: "/opt/activitywatch/webui-ru" aw_server_data_dir: "/var/lib/activitywatch" -aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db" aw_server_log_dir: "/var/log/activitywatch" aw_server_user: "activitywatch" aw_server_group: "activitywatch" @@ -13,17 +12,9 @@ aw_worktime_timezone: "Europe/Moscow" aw_repo_root: "{{ playbook_dir | dirname }}" -# Применить базовые категории и views для рабочего времени через AW settings API. -# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes. -aw_apply_worktime_settings: true - -# Дополнительные origin для aw-server-rust CORS. -# Обязательно включите тот origin, с которого реально открывается Web UI. -aw_server_cors_origins: - - "http://127.0.0.1:5600" - - "http://localhost:5600" - - "http://10.10.10.13:5600" - - "http://aw-server:5600" +# Опционально: применить базовые категории и views для рабочего времени через AW settings API. +# Внимание: это перезаписывает существующие server-side settings/classes/views. +aw_apply_worktime_settings: false # Опциональные значения периода рабочего времени в Web UI. # startOfDay задаёт границу дня и стартовое время окна отчёта. @@ -33,5 +24,3 @@ aw_server_cors_origins: aw_worktime_from: "08:00" aw_worktime_to: "17:00" aw_worktime_start_of_day: "{{ aw_worktime_from }}" -aw_server_always_active_pattern: "aw-watcher-window" -aw_server_landingpage: "/activity/SHARKON2025/view/" diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml index 707b658..30c20c3 100644 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml +++ b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml @@ -23,7 +23,6 @@ aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin" aw_windows_state_root: "C:\\ProgramData\\AWatch-rus" aw_windows_afk_enabled: true aw_windows_window_enabled: true -aw_windows_file_ops_enabled: true aw_windows_local_agent_logs_enabled: false aw_windows_incident_capture_enabled: true aw_windows_incident_screenshot_enabled: true diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml index 1ac786e..40c6382 100644 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml +++ b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml @@ -11,9 +11,6 @@ - activitywatch-server.service - aw-worktime-api.py - aw-worktime-api.service - - aw-worktime-ui-bridge.py - - aw-worktime-ui-bridge.service - - aw-worktime-ui-bridge.timer - aw-worktime-panel.js - aw-server.env.example - aw-ru-patch.js diff --git a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml index e0ff9bc..5452935 100644 --- a/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml +++ b/install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml @@ -11,9 +11,6 @@ - activitywatch-server.service - aw-worktime-api.py - aw-worktime-api.service - - aw-worktime-ui-bridge.py - - aw-worktime-ui-bridge.service - - aw-worktime-ui-bridge.timer - aw-worktime-panel.js - aw-server.env.example - aw-ru-patch.js diff --git a/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service b/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service index 89d31e3..e8f26e3 100755 --- a/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service +++ b/install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service @@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env User=__AW_SERVER_USER__ Group=__AW_SERVER_GROUP__ WorkingDirectory=__AW_SERVER_DATA_DIR__ -ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"' +ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"' Restart=on-failure RestartSec=5s StateDirectory=activitywatch @@ -17,7 +17,7 @@ LogsDirectory=activitywatch NoNewPrivileges=true PrivateTmp=true ProtectSystem=full -ProtectHome=read-only +ProtectHome=true LimitNOFILE=65535 [Install] diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json b/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json index 7432f60..f3d0118 100644 --- a/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json +++ b/install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json @@ -29,21 +29,6 @@ { "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" } ] }, - { - "id": "linux-remote", - "name": "Linux remote workers", - "description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.", - "patterns": [ - "^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)" - ], - "links": [ - { "label": "Активность", "type": "activity" }, - { "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" }, - { "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" }, - { "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" }, - { "label": "Все бакеты", "type": "buckets" } - ] - }, { "id": "virtual-infra", "name": "Virtual servers + Proxmox", diff --git a/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js b/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js index 3f06563..28145bd 100755 --- a/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js +++ b/install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js @@ -370,16 +370,6 @@ return /^pve[-_]/i.test(String(host || "")); } - function isLikelyClientHost(host) { - const value = String(host || "").trim(); - if (!value) return false; - if (/^(?:unknown|undefined|null)$/i.test(value)) return false; - if (/^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)$/i.test(value)) return false; - if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false; - if (value.indexOf(":") !== -1 && /^[0-9a-f:\[\]]+$/i.test(value)) return false; - return true; - } - function enforceSafeActivityViewForPveHost() { const hash = window.location.hash || ""; const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i); @@ -396,9 +386,9 @@ function getDlpHostFromSettings(settings) { const routeHost = getCurrentHostFromHash(); - if (isLikelyClientHost(routeHost)) return routeHost; + if (routeHost) return routeHost; const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash()); - if (isLikelyClientHost(bucketHost)) return bucketHost; + if (bucketHost) return bucketHost; return getTrendsHostFromSettings(settings); } @@ -690,19 +680,6 @@ { label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" } ] }, - { - id: "linux-remote", - name: "Linux remote workers", - description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.", - patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"], - links: [ - { label: "Активность", type: "activity" }, - { label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" }, - { label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" }, - { label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" }, - { label: "Все бакеты", type: "buckets" } - ] - }, { id: "virtual-infra", name: "Virtual servers + Proxmox", @@ -763,15 +740,7 @@ const prefixes = [ "aw-watcher-window_", "aw-watcher-afk_", - "aw-console-commands_", - "aw-ssh-sessions_", - "aw-linux-web-context_", - "aw-detmir-web-category_", "aw-dlp-endpoint-signals_", - "aw-session-events_", - "aw-worktime-sessions_", - "aw-pve-webadmin-events_", - "aw-pve-task-events_", "aw-dlp-incidents_", "aw-pfsense-health_", "aw-pfsense-gateways_", @@ -801,27 +770,7 @@ return result; } - function hostHasBucketPrefix(hostBuckets, prefix) { - return (hostBuckets || []).some(function (bucketId) { - return String(bucketId || "").indexOf(prefix) === 0; - }); - } - - function matchHostGroup(host, groups, hostBuckets) { - const bucketList = hostBuckets || []; - if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) { - return "windows-rdp"; - } - if ( - hostHasBucketPrefix(bucketList, "aw-console-commands_") || - hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") || - hostHasBucketPrefix(bucketList, "aw-linux-web-context_") || - hostHasBucketPrefix(bucketList, "aw-detmir-web-category_") - ) { - if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) { - return "linux-remote"; - } - } + function matchHostGroup(host, groups) { for (const group of groups) { const patterns = Array.isArray(group.patterns) ? group.patterns : []; for (const pattern of patterns) { @@ -864,7 +813,7 @@ grouped.set("__ungrouped__", []); Array.from(hostBuckets.keys()).sort().forEach(function (host) { - const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__"; + const groupId = matchHostGroup(host, groups) || "__ungrouped__"; grouped.get(groupId).push(host); }); @@ -920,7 +869,7 @@ center.setAttribute("data-aw-ru-host-groups", "1"); center.innerHTML = '

Разделы хостов

' + - '

Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.

' + + '

Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.

' + '

Загрузка...

'; heading.parentElement.insertBefore(center, heading.nextSibling); } @@ -1476,8 +1425,7 @@ if (!settings || typeof settings !== "object") return ""; const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : ""; const match = landingpage.match(/\/activity\/([^/]+)/); - const host = match && match[1] ? decodeURIComponent(match[1]) : ""; - return isLikelyClientHost(host) ? host : ""; + return match && match[1] ? match[1] : ""; } function getTrendsPath(hash) { @@ -1544,7 +1492,8 @@ .map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); }) .filter(Boolean) .filter(function (host) { return !/^unknown$/i.test(host); }); - if (isLikelyClientHost(settingsHost) && hosts.indexOf(settingsHost) >= 0) return settingsHost; + if (settingsHost && hosts.indexOf(settingsHost) >= 0) return settingsHost; + if (settingsHost) return settingsHost; hosts.sort(); return hosts[0] || ""; } @@ -1584,8 +1533,7 @@ window.fetch = function (input, init) { try { const url = typeof input === "string" ? input : String(input && input.url || ""); - const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || ""); - if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") { + if (/\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") { init = Object.assign({}, init, { body: rewriteUnknownCategoryBuilderQueryBody(init.body) }); @@ -1609,8 +1557,7 @@ proto.send = function (body) { try { const url = String(this.__awRuUrl || ""); - const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || ""); - if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && typeof body === "string") { + if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") { body = rewriteUnknownCategoryBuilderQueryBody(body); } } catch (error) { diff --git a/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh b/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh index 4ac7a30..50ba3e7 100755 --- a/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh +++ b/install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh @@ -26,9 +26,6 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json" CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json" WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py" WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service" -WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py" -WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service" -WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer" for var_name in "${required_vars[@]}"; do if [[ -z "${!var_name:-}" ]]; then @@ -106,24 +103,6 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then systemctl --no-pager --full status aw-worktime-api.service || true fi -if [[ -f "$WORKTIME_UI_BRIDGE_SRC" ]]; then - install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py -fi - -if [[ -f "$WORKTIME_UI_BRIDGE_SERVICE_SRC" ]]; then - install -m 0644 "$WORKTIME_UI_BRIDGE_SERVICE_SRC" /etc/systemd/system/aw-worktime-ui-bridge.service -fi - -if [[ -f "$WORKTIME_UI_BRIDGE_TIMER_SRC" ]]; then - install -m 0644 "$WORKTIME_UI_BRIDGE_TIMER_SRC" /etc/systemd/system/aw-worktime-ui-bridge.timer - systemctl daemon-reload - systemctl disable --now aw-worktime-afk-bridge.timer >/dev/null 2>&1 || true - systemctl enable aw-worktime-ui-bridge.timer - systemctl restart aw-worktime-ui-bridge.timer - systemctl start aw-worktime-ui-bridge.service || true - systemctl --no-pager --full status aw-worktime-ui-bridge.timer || true -fi - for _ in $(seq 1 20); do if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then break diff --git a/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json b/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json index 11ee810..b6361f4 100644 --- a/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json +++ b/install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json @@ -20,7 +20,7 @@ "name": ["Работа", "Документы"], "rule": { "type": "regex", - "regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat", + "regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat", "ignore_case": true }, "data": { "color": "#2E7D32" } @@ -40,7 +40,7 @@ "name": ["Работа", "Администрирование"], "rule": { "type": "regex", - "regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b", + "regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b", "ignore_case": true }, "data": { "color": "#6D4C41" } @@ -56,7 +56,7 @@ "name": ["Интернет", "Браузер"], "rule": { "type": "regex", - "regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b", + "regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b", "ignore_case": true }, "data": { "color": "#00897B" } @@ -82,7 +82,7 @@ "name": ["ActivityWatch"], "rule": { "type": "regex", - "regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b", + "regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b", "ignore_case": true }, "data": {} diff --git a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 index 5f1e7fb..e3a30b5 100755 --- a/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 +++ b/install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1 @@ -306,9 +306,6 @@ function Copy-ActivityWatchCollectorAssets { $resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force } - else { - Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force - } if ($CustomPolicySource) { $resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop @@ -537,7 +534,11 @@ function Get-CollectorPowerShellProcessCount { function New-LaunchLock { param([string]`$StateRoot, [int]`$SessionId) - `$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId) + if (-not (Test-Path -LiteralPath `$StateRoot)) { + New-Item -Path `$StateRoot -ItemType Directory -Force | Out-Null + } + + `$lockPath = Join-Path `$StateRoot ("launch-watchers-session-{0}.lock" -f `$SessionId) if (Test-Path -LiteralPath `$lockPath) { try { `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json @@ -730,11 +731,13 @@ function Start-CollectorScriptIfNeeded { return } - `$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null } - `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass') - if (`$staParam) { `$argumentList += `$staParam } - `$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath) - Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden + Start-Process -FilePath `$PowerShellExe -ArgumentList @( + '-NoProfile', + '-WindowStyle', 'Hidden', + '-ExecutionPolicy', 'Bypass', + '-File', `$ScriptPath, + '-ConfigPath', `$ConfigPath + ) -WindowStyle Hidden } `$config = Get-DeploymentConfig -Path `$ConfigPath diff --git a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 index e7418d0..d0c809f 100755 --- a/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1 @@ -1,122 +1,4 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$RulesPath, - [string]$PolicyPath, - [string]$LogPath, - [string]$IncidentLogPath, - [int]$PollSeconds, - [int]$PulseSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes - -Add-Type @" -using System; -using System.Runtime.InteropServices; -using System.Text; - -public static class NativeAwMethods { - [DllImport("user32.dll")] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - public static extern int GetWindowTextLength(IntPtr hWnd); -} -"@ - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } -$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentLogPath = $resolvedIncidentLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:IncidentState = @{} -$script:DlpRules = @() -$script:DlpDefaults = [ordered]@{ - enabled = $false - cooldownSeconds = 300 - action = 'log' - severity = 'low' -} -$script:BrowserMap = @{ - msedge = 'edge' - chrome = 'chrome' - brave = 'brave' - vivaldi = 'vivaldi' - opera = 'opera' - firefox = 'firefox' -} -$script:CategoryRules = @( - @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } - @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } - @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } - @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } - @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } - @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } - @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } - @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } - @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } - @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } -) - -function Write-CollectorLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { Write-Error [CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -659,7 +541,7 @@ function Send-DlpIncidentHeartbeat { } + $captureData } | ConvertTo-Json -Depth 5 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null } function Get-FileSha256Hex { @@ -819,26 +701,13 @@ function Ensure-Bucket { return } - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - $body = @{ client = $ClientName type = $BucketType hostname = $script:Hostname } | ConvertTo-Json -Compress - try { - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body | Out-Null $script:KnownBuckets[$BucketId] = $true } @@ -864,7 +733,7 @@ function Send-Heartbeat { } } | ConvertTo-Json -Depth 4 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null } function Send-CategoryHeartbeat { @@ -901,3225 +770,7 @@ function Send-CategoryHeartbeat { } } | ConvertTo-Json -Depth 4 -Compress - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -Load-CustomCategoryRules -Path $resolvedRulesPath -Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) - -while ($true) { - try { - $context = Get-ForegroundWindowContext - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } - - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } - - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } - } - } - } - } - } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } -} - -function Write-DlpIncidentLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$RulesPath, - [string]$PolicyPath, - [string]$LogPath, - [string]$IncidentLogPath, - [int]$PollSeconds, - [int]$PulseSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes - -Add-Type @" -using System; -using System.Runtime.InteropServices; -using System.Text; - -public static class NativeAwMethods { - [DllImport("user32.dll")] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - public static extern int GetWindowTextLength(IntPtr hWnd); -} -"@ - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } -$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentLogPath = $resolvedIncidentLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:IncidentState = @{} -$script:DlpRules = @() -$script:DlpDefaults = [ordered]@{ - enabled = $false - cooldownSeconds = 300 - action = 'log' - severity = 'low' -} -$script:BrowserMap = @{ - msedge = 'edge' - chrome = 'chrome' - brave = 'brave' - vivaldi = 'vivaldi' - opera = 'opera' - firefox = 'firefox' -} -$script:CategoryRules = @( - @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } - @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } - @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } - @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } - @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } - @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } - @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } - @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } - @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } - @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } -) - -function Write-CollectorLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Write-DlpIncidentLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Test-DomainMatch { - param( - [string]$DomainHost, - [string]$RuleDomain - ) - - if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { - return $false - } - - $left = $DomainHost.ToLowerInvariant() - $right = $RuleDomain.ToLowerInvariant() - return $left -eq $right -or $left.EndsWith('.' + $right) -} - -function Get-HostFromUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - try { - $uri = [Uri]$Url - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost.StartsWith('www.')) { - return $uriHost.Substring(4) - } - - return $uriHost - } - catch { - return $null - } -} - -function Get-RootDomain { - param([string]$DomainHost) - - if ([string]::IsNullOrWhiteSpace($DomainHost)) { - return $null - } - - $parts = $DomainHost.Split('.') - if ($parts.Count -le 2) { - return $DomainHost - } - - $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() - $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') - if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { - return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() - } - - return $suffix -} - -function ConvertTo-NormalizedUrl { - param([AllowNull()][string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $candidate = $Value.Trim() - if ($candidate.Length -lt 4) { - return $null - } - - if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { - return $null - } - - if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { - return $candidate - } - - if ($candidate -match '^(?i)localhost([/:]|$)') { - return "http://$candidate" - } - - if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { - return "https://$candidate" - } - - return $null -} - -function Load-CustomCategoryRules { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $rules = @() - - if ($parsed.rules) { - $sourceRules = @($parsed.rules) - } - elseif ($parsed -is [System.Collections.IEnumerable]) { - $sourceRules = @($parsed) - } - else { - $sourceRules = @() - } - - foreach ($rule in $sourceRules) { - if (-not $rule) { - continue - } - - $name = [string]$rule.name - $group = [string]$rule.group - $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) - - if ($name -and $group -and $domains.Count -gt 0) { - $rules += @{ - Name = $name - Group = $group - Domains = $domains - } - } - } - - if ($rules.Count -gt 0) { - $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) - } - } - catch { - Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) - } -} - -function Get-WebCategory { - param([string]$DomainHost) - - foreach ($rule in $script:CategoryRules) { - foreach ($domain in $rule.Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return [pscustomobject]@{ - Name = [string]$rule.Name - Group = [string]$rule.Group - Rule = [string]$domain - } - } - } - } - - return [pscustomobject]@{ - Name = 'uncategorized' - Group = 'neutral' - Rule = 'none' - } -} - -function Test-DomainListMatch { - param( - [string]$DomainHost, - [string[]]$Domains - ) - - if (-not $Domains -or $Domains.Count -eq 0) { - return $false - } - - foreach ($domain in $Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return $true - } - } - - return $false -} - -function Test-DlpRuleTimeWindow { - param( - [int]$CurrentHour, - [AllowNull()][int]$HourFrom, - [AllowNull()][int]$HourTo - ) - - if ($null -eq $HourFrom -or $null -eq $HourTo) { - return $true - } - - if ($HourFrom -eq $HourTo) { - return $true - } - - if ($HourFrom -lt $HourTo) { - return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) - } - - return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) -} - -function Load-DlpPolicy { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $defaults = $parsed.defaults - if ($defaults) { - if ($defaults.PSObject.Properties.Name -contains 'enabled') { - $script:DlpDefaults.enabled = [bool]$defaults.enabled - } - if ($defaults.cooldownSeconds) { - $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds - } - if ($defaults.action) { - $script:DlpDefaults.action = [string]$defaults.action - } - if ($defaults.severity) { - $script:DlpDefaults.severity = [string]$defaults.severity - } - } - - $loaded = @() - foreach ($rule in @($parsed.rules)) { - if (-not $rule) { continue } - $when = $rule.when - if (-not $when) { - $when = [pscustomobject]@{} - } - $loaded += [pscustomobject]@{ - id = [string]$rule.id - enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } - action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } - severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } - message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" } - cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } - when = [pscustomobject]@{ - domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } - titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } - hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } - hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } - } - } - } - - $script:DlpRules = @($loaded) - Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) - } - catch { - Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) - } -} - -function Test-DlpRuleMatch { - param( - [pscustomobject]$Rule, - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $Rule.enabled) { - return $false - } - - $when = $Rule.when - $currentHour = (Get-Date).Hour - if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { - return $false - } - - if ($when.domains.Count -gt 0) { - $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) - if (-not $domainMatched) { - return $false - } - } - - if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { - return $false - } - - if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { - return $false - } - - if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { - return $false - } - - if ($when.urlRegex) { - if (-not ($Url -match $when.urlRegex)) { - return $false - } - } - - if ($when.titleRegex) { - if (-not ($Title -match $when.titleRegex)) { - return $false - } - } - - return $true -} - -function Get-DlpDecision { - param( - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $script:DlpDefaults.enabled) { - return $null - } - - foreach ($rule in $script:DlpRules) { - if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { - return $rule - } - } - - return $null -} - -function Should-EmitIncident { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:IncidentState.ContainsKey($Fingerprint)) { - $last = [datetime]$script:IncidentState[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:IncidentState[$Fingerprint] = $now - return $true -} - -function Send-DlpIncidentHeartbeat { - param( - [pscustomobject]$Decision, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' - } - catch { - } - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = [string]$Decision.id - action = [string]$Decision.action - severity = [string]$Decision.severity - message = [string]$Decision.message - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - source = 'uia-native-dlp' - } + $captureData - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) - return @{} - } -} - -function Get-ForegroundWindowContext { - $handle = [NativeAwMethods]::GetForegroundWindow() - if ($handle -eq [IntPtr]::Zero) { - return $null - } - - $processId = [uint32]0 - [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) - if (-not $processId) { - return $null - } - - $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue - if (-not $process) { - return $null - } - - $textLength = [NativeAwMethods]::GetWindowTextLength($handle) - $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) - [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) - - return [pscustomobject]@{ - Handle = $handle - ProcessName = $process.ProcessName.ToLowerInvariant() - Title = $builder.ToString() - } -} - -function Get-BrowserUrlFromWindow { - param([IntPtr]$Handle) - - $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) - if (-not $root) { - return $null - } - - $editCondition = [System.Windows.Automation.PropertyCondition]::new( - [System.Windows.Automation.AutomationElement]::ControlTypeProperty, - [System.Windows.Automation.ControlType]::Edit - ) - - $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) - foreach ($edit in $edits) { - $valuePattern = $null - if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { - $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value - if ($candidate) { - return $candidate - } - } - - $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name - if ($candidateFromName) { - return $candidateFromName - } - } - - return $null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType = 'web.tab.current' - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-Heartbeat { - param( - [string]$BucketId, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName - ) - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Send-CategoryHeartbeat { - param( - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup, - [string]$CategoryRule - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - categoryRule = $CategoryRule - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -Load-CustomCategoryRules -Path $resolvedRulesPath -Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) - -while ($true) { - try { - $context = Get-ForegroundWindowContext - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } - - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } - - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } - } - } - } - } - } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } -} - -function Test-DomainMatch { - param( - [string]$DomainHost, - [string]$RuleDomain - ) - - if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { - return $false - } - - $left = $DomainHost.ToLowerInvariant() - $right = $RuleDomain.ToLowerInvariant() - return $left -eq $right -or $left.EndsWith('.' + $right) -} - -function Get-HostFromUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - try { - $uri = [Uri]$Url - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost.StartsWith('www.')) { - return $uriHost.Substring(4) - } - - return $uriHost - } - catch { - return $null - } -} - -function Get-RootDomain { - param([string]$DomainHost) - - if ([string]::IsNullOrWhiteSpace($DomainHost)) { - return $null - } - - $parts = $DomainHost.Split('.') - if ($parts.Count -le 2) { - return $DomainHost - } - - $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() - $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') - if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { - return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() - } - - return $suffix -} - -function ConvertTo-NormalizedUrl { - param([AllowNull()][string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $candidate = $Value.Trim() - if ($candidate.Length -lt 4) { - return $null - } - - if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { - return $null - } - - if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { - return $candidate - } - - if ($candidate -match '^(?i)localhost([/:]|$)') { - return "http://$candidate" - } - - if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { - return "https://$candidate" - } - - return $null -} - -function Load-CustomCategoryRules { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $rules = @() - - if ($parsed.rules) { - $sourceRules = @($parsed.rules) - } - elseif ($parsed -is [System.Collections.IEnumerable]) { - $sourceRules = @($parsed) - } - else { - $sourceRules = @() - } - - foreach ($rule in $sourceRules) { - if (-not $rule) { - continue - } - - $name = [string]$rule.name - $group = [string]$rule.group - $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) - - if ($name -and $group -and $domains.Count -gt 0) { - $rules += @{ - Name = $name - Group = $group - Domains = $domains - } - } - } - - if ($rules.Count -gt 0) { - $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) - } - } - catch { - Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) - } -} - -function Get-WebCategory { - param([string]$DomainHost) - - foreach ($rule in $script:CategoryRules) { - foreach ($domain in $rule.Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return [pscustomobject]@{ - Name = [string]$rule.Name - Group = [string]$rule.Group - Rule = [string]$domain - } - } - } - } - - return [pscustomobject]@{ - Name = 'uncategorized' - Group = 'neutral' - Rule = 'none' - } -} - -function Test-DomainListMatch { - param( - [string]$DomainHost, - [string[]]$Domains - ) - - if (-not $Domains -or $Domains.Count -eq 0) { - return $false - } - - foreach ($domain in $Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return $true - } - } - - return $false -} - -function Test-DlpRuleTimeWindow { - param( - [int]$CurrentHour, - [AllowNull()][int]$HourFrom, - [AllowNull()][int]$HourTo - ) - - if ($null -eq $HourFrom -or $null -eq $HourTo) { - return $true - } - - if ($HourFrom -eq $HourTo) { - return $true - } - - if ($HourFrom -lt $HourTo) { - return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) - } - - return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) -} - -function Load-DlpPolicy { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $defaults = $parsed.defaults - if ($defaults) { - if ($defaults.PSObject.Properties.Name -contains 'enabled') { - $script:DlpDefaults.enabled = [bool]$defaults.enabled - } - if ($defaults.cooldownSeconds) { - $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds - } - if ($defaults.action) { - $script:DlpDefaults.action = [string]$defaults.action - } - if ($defaults.severity) { - $script:DlpDefaults.severity = [string]$defaults.severity - } - } - - $loaded = @() - foreach ($rule in @($parsed.rules)) { - if (-not $rule) { continue } - $when = $rule.when - if (-not $when) { - $when = [pscustomobject]@{} - } - $loaded += [pscustomobject]@{ - id = [string]$rule.id - enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } - action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } - severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } - message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" } - cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } - when = [pscustomobject]@{ - domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } - titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } - hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } - hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } - } - } - } - - $script:DlpRules = @($loaded) - Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) - } - catch { - Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) - } -} - -function Test-DlpRuleMatch { - param( - [pscustomobject]$Rule, - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $Rule.enabled) { - return $false - } - - $when = $Rule.when - $currentHour = (Get-Date).Hour - if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { - return $false - } - - if ($when.domains.Count -gt 0) { - $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) - if (-not $domainMatched) { - return $false - } - } - - if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { - return $false - } - - if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { - return $false - } - - if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { - return $false - } - - if ($when.urlRegex) { - if (-not ($Url -match $when.urlRegex)) { - return $false - } - } - - if ($when.titleRegex) { - if (-not ($Title -match $when.titleRegex)) { - return $false - } - } - - return $true -} - -function Get-DlpDecision { - param( - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $script:DlpDefaults.enabled) { - return $null - } - - foreach ($rule in $script:DlpRules) { - if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { - return $rule - } - } - - return $null -} - -function Should-EmitIncident { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:IncidentState.ContainsKey($Fingerprint)) { - $last = [datetime]$script:IncidentState[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:IncidentState[$Fingerprint] = $now - return $true -} - -function Send-DlpIncidentHeartbeat { - param( - [pscustomobject]$Decision, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$RulesPath, - [string]$PolicyPath, - [string]$LogPath, - [string]$IncidentLogPath, - [int]$PollSeconds, - [int]$PulseSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes - -Add-Type @" -using System; -using System.Runtime.InteropServices; -using System.Text; - -public static class NativeAwMethods { - [DllImport("user32.dll")] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - public static extern int GetWindowTextLength(IntPtr hWnd); -} -"@ - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } -$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentLogPath = $resolvedIncidentLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:IncidentState = @{} -$script:DlpRules = @() -$script:DlpDefaults = [ordered]@{ - enabled = $false - cooldownSeconds = 300 - action = 'log' - severity = 'low' -} -$script:BrowserMap = @{ - msedge = 'edge' - chrome = 'chrome' - brave = 'brave' - vivaldi = 'vivaldi' - opera = 'opera' - firefox = 'firefox' -} -$script:CategoryRules = @( - @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } - @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } - @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } - @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } - @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } - @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } - @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } - @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } - @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } - @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } -) - -function Write-CollectorLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Write-DlpIncidentLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Test-DomainMatch { - param( - [string]$DomainHost, - [string]$RuleDomain - ) - - if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { - return $false - } - - $left = $DomainHost.ToLowerInvariant() - $right = $RuleDomain.ToLowerInvariant() - return $left -eq $right -or $left.EndsWith('.' + $right) -} - -function Get-HostFromUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - try { - $uri = [Uri]$Url - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost.StartsWith('www.')) { - return $uriHost.Substring(4) - } - - return $uriHost - } - catch { - return $null - } -} - -function Get-RootDomain { - param([string]$DomainHost) - - if ([string]::IsNullOrWhiteSpace($DomainHost)) { - return $null - } - - $parts = $DomainHost.Split('.') - if ($parts.Count -le 2) { - return $DomainHost - } - - $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() - $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') - if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { - return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() - } - - return $suffix -} - -function ConvertTo-NormalizedUrl { - param([AllowNull()][string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $candidate = $Value.Trim() - if ($candidate.Length -lt 4) { - return $null - } - - if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { - return $null - } - - if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { - return $candidate - } - - if ($candidate -match '^(?i)localhost([/:]|$)') { - return "http://$candidate" - } - - if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { - return "https://$candidate" - } - - return $null -} - -function Load-CustomCategoryRules { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $rules = @() - - if ($parsed.rules) { - $sourceRules = @($parsed.rules) - } - elseif ($parsed -is [System.Collections.IEnumerable]) { - $sourceRules = @($parsed) - } - else { - $sourceRules = @() - } - - foreach ($rule in $sourceRules) { - if (-not $rule) { - continue - } - - $name = [string]$rule.name - $group = [string]$rule.group - $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) - - if ($name -and $group -and $domains.Count -gt 0) { - $rules += @{ - Name = $name - Group = $group - Domains = $domains - } - } - } - - if ($rules.Count -gt 0) { - $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) - } - } - catch { - Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) - } -} - -function Get-WebCategory { - param([string]$DomainHost) - - foreach ($rule in $script:CategoryRules) { - foreach ($domain in $rule.Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return [pscustomobject]@{ - Name = [string]$rule.Name - Group = [string]$rule.Group - Rule = [string]$domain - } - } - } - } - - return [pscustomobject]@{ - Name = 'uncategorized' - Group = 'neutral' - Rule = 'none' - } -} - -function Test-DomainListMatch { - param( - [string]$DomainHost, - [string[]]$Domains - ) - - if (-not $Domains -or $Domains.Count -eq 0) { - return $false - } - - foreach ($domain in $Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return $true - } - } - - return $false -} - -function Test-DlpRuleTimeWindow { - param( - [int]$CurrentHour, - [AllowNull()][int]$HourFrom, - [AllowNull()][int]$HourTo - ) - - if ($null -eq $HourFrom -or $null -eq $HourTo) { - return $true - } - - if ($HourFrom -eq $HourTo) { - return $true - } - - if ($HourFrom -lt $HourTo) { - return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) - } - - return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) -} - -function Load-DlpPolicy { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $defaults = $parsed.defaults - if ($defaults) { - if ($defaults.PSObject.Properties.Name -contains 'enabled') { - $script:DlpDefaults.enabled = [bool]$defaults.enabled - } - if ($defaults.cooldownSeconds) { - $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds - } - if ($defaults.action) { - $script:DlpDefaults.action = [string]$defaults.action - } - if ($defaults.severity) { - $script:DlpDefaults.severity = [string]$defaults.severity - } - } - - $loaded = @() - foreach ($rule in @($parsed.rules)) { - if (-not $rule) { continue } - $when = $rule.when - if (-not $when) { - $when = [pscustomobject]@{} - } - $loaded += [pscustomobject]@{ - id = [string]$rule.id - enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } - action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } - severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } - message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" } - cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } - when = [pscustomobject]@{ - domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } - titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } - hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } - hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } - } - } - } - - $script:DlpRules = @($loaded) - Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) - } - catch { - Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) - } -} - -function Test-DlpRuleMatch { - param( - [pscustomobject]$Rule, - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $Rule.enabled) { - return $false - } - - $when = $Rule.when - $currentHour = (Get-Date).Hour - if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { - return $false - } - - if ($when.domains.Count -gt 0) { - $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) - if (-not $domainMatched) { - return $false - } - } - - if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { - return $false - } - - if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { - return $false - } - - if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { - return $false - } - - if ($when.urlRegex) { - if (-not ($Url -match $when.urlRegex)) { - return $false - } - } - - if ($when.titleRegex) { - if (-not ($Title -match $when.titleRegex)) { - return $false - } - } - - return $true -} - -function Get-DlpDecision { - param( - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $script:DlpDefaults.enabled) { - return $null - } - - foreach ($rule in $script:DlpRules) { - if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { - return $rule - } - } - - return $null -} - -function Should-EmitIncident { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:IncidentState.ContainsKey($Fingerprint)) { - $last = [datetime]$script:IncidentState[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:IncidentState[$Fingerprint] = $now - return $true -} - -function Send-DlpIncidentHeartbeat { - param( - [pscustomobject]$Decision, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' - } - catch { - } - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = [string]$Decision.id - action = [string]$Decision.action - severity = [string]$Decision.severity - message = [string]$Decision.message - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - source = 'uia-native-dlp' - } + $captureData - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) - return @{} - } -} - -function Get-ForegroundWindowContext { - $handle = [NativeAwMethods]::GetForegroundWindow() - if ($handle -eq [IntPtr]::Zero) { - return $null - } - - $processId = [uint32]0 - [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) - if (-not $processId) { - return $null - } - - $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue - if (-not $process) { - return $null - } - - $textLength = [NativeAwMethods]::GetWindowTextLength($handle) - $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) - [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) - - return [pscustomobject]@{ - Handle = $handle - ProcessName = $process.ProcessName.ToLowerInvariant() - Title = $builder.ToString() - } -} - -function Get-BrowserUrlFromWindow { - param([IntPtr]$Handle) - - $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) - if (-not $root) { - return $null - } - - $editCondition = [System.Windows.Automation.PropertyCondition]::new( - [System.Windows.Automation.AutomationElement]::ControlTypeProperty, - [System.Windows.Automation.ControlType]::Edit - ) - - $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) - foreach ($edit in $edits) { - $valuePattern = $null - if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { - $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value - if ($candidate) { - return $candidate - } - } - - $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name - if ($candidateFromName) { - return $candidateFromName - } - } - - return $null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType = 'web.tab.current' - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-Heartbeat { - param( - [string]$BucketId, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName - ) - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Send-CategoryHeartbeat { - param( - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup, - [string]$CategoryRule - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - categoryRule = $CategoryRule - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -Load-CustomCategoryRules -Path $resolvedRulesPath -Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) - -while ($true) { - try { - $context = Get-ForegroundWindowContext - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } - - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } - - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } - } - } - } - } - } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = [string]$Decision.id - action = [string]$Decision.action - severity = [string]$Decision.severity - message = [string]$Decision.message - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - source = 'uia-native-dlp' - } + $captureData - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) - return @{} - } -} - -function Get-ForegroundWindowContext { - $handle = [NativeAwMethods]::GetForegroundWindow() - if ($handle -eq [IntPtr]::Zero) { - return $null - } - - $processId = [uint32]0 - [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) - if (-not $processId) { - return $null - } - - $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue - if (-not $process) { - return $null - } - - $textLength = [NativeAwMethods]::GetWindowTextLength($handle) - $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) - [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) - - return [pscustomobject]@{ - Handle = $handle - ProcessName = $process.ProcessName.ToLowerInvariant() - Title = $builder.ToString() - } -} - -function Get-BrowserUrlFromWindow { - param([IntPtr]$Handle) - - $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) - if (-not $root) { - return $null - } - - $editCondition = [System.Windows.Automation.PropertyCondition]::new( - [System.Windows.Automation.AutomationElement]::ControlTypeProperty, - [System.Windows.Automation.ControlType]::Edit - ) - - $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) - foreach ($edit in $edits) { - $valuePattern = $null - if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { - $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value - if ($candidate) { - return $candidate - } - } - - $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name - if ($candidateFromName) { - return $candidateFromName - } - } - - return $null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType = 'web.tab.current' - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$RulesPath, - [string]$PolicyPath, - [string]$LogPath, - [string]$IncidentLogPath, - [int]$PollSeconds, - [int]$PulseSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -Add-Type -AssemblyName UIAutomationClient -Add-Type -AssemblyName UIAutomationTypes - -Add-Type @" -using System; -using System.Runtime.InteropServices; -using System.Text; - -public static class NativeAwMethods { - [DllImport("user32.dll")] - public static extern IntPtr GetForegroundWindow(); - - [DllImport("user32.dll")] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); - - [DllImport("user32.dll")] - public static extern int GetWindowTextLength(IntPtr hWnd); -} -"@ - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'Укажите ServerHost или подготовьте deployment-config.json.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\AWatch-rus\web-category-rules.json' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } -$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentLogPath = $resolvedIncidentLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false -$script:IncidentState = @{} -$script:DlpRules = @() -$script:DlpDefaults = [ordered]@{ - enabled = $false - cooldownSeconds = 300 - action = 'log' - severity = 'low' -} -$script:BrowserMap = @{ - msedge = 'edge' - chrome = 'chrome' - brave = 'brave' - vivaldi = 'vivaldi' - opera = 'opera' - firefox = 'firefox' -} -$script:CategoryRules = @( - @{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') } - @{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') } - @{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') } - @{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') } - @{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') } - @{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') } - @{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') } - @{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') } - @{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') } - @{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') } -) - -function Write-CollectorLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Write-DlpIncidentLog { - param([string]$Message) - - if (-not $script:LocalAgentLogsEnabled) { - return - } - - try { - Add-Content -LiteralPath $script:IncidentLogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Test-DomainMatch { - param( - [string]$DomainHost, - [string]$RuleDomain - ) - - if ([string]::IsNullOrWhiteSpace($DomainHost) -or [string]::IsNullOrWhiteSpace($RuleDomain)) { - return $false - } - - $left = $DomainHost.ToLowerInvariant() - $right = $RuleDomain.ToLowerInvariant() - return $left -eq $right -or $left.EndsWith('.' + $right) -} - -function Get-HostFromUrl { - param([string]$Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - try { - $uri = [Uri]$Url - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost.StartsWith('www.')) { - return $uriHost.Substring(4) - } - - return $uriHost - } - catch { - return $null - } -} - -function Get-RootDomain { - param([string]$DomainHost) - - if ([string]::IsNullOrWhiteSpace($DomainHost)) { - return $null - } - - $parts = $DomainHost.Split('.') - if ($parts.Count -le 2) { - return $DomainHost - } - - $suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant() - $compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn') - if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) { - return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant() - } - - return $suffix -} - -function ConvertTo-NormalizedUrl { - param([AllowNull()][string]$Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $candidate = $Value.Trim() - if ($candidate.Length -lt 4) { - return $null - } - - if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') { - return $null - } - - if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') { - return $candidate - } - - if ($candidate -match '^(?i)localhost([/:]|$)') { - return "http://$candidate" - } - - if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') { - return "https://$candidate" - } - - return $null -} - -function Load-CustomCategoryRules { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $rules = @() - - if ($parsed.rules) { - $sourceRules = @($parsed.rules) - } - elseif ($parsed -is [System.Collections.IEnumerable]) { - $sourceRules = @($parsed) - } - else { - $sourceRules = @() - } - - foreach ($rule in $sourceRules) { - if (-not $rule) { - continue - } - - $name = [string]$rule.name - $group = [string]$rule.group - $domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) - - if ($name -and $group -and $domains.Count -gt 0) { - $rules += @{ - Name = $name - Group = $group - Domains = $domains - } - } - } - - if ($rules.Count -gt 0) { - $script:CategoryRules = @($rules) + @($script:CategoryRules) - Write-CollectorLog ("пользовательские правила загружены: {0}" -f $rules.Count) - } - } - catch { - Write-CollectorLog ("не удалось загрузить пользовательские правила: {0}" -f $_.Exception.Message) - } -} - -function Get-WebCategory { - param([string]$DomainHost) - - foreach ($rule in $script:CategoryRules) { - foreach ($domain in $rule.Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return [pscustomobject]@{ - Name = [string]$rule.Name - Group = [string]$rule.Group - Rule = [string]$domain - } - } - } - } - - return [pscustomobject]@{ - Name = 'uncategorized' - Group = 'neutral' - Rule = 'none' - } -} - -function Test-DomainListMatch { - param( - [string]$DomainHost, - [string[]]$Domains - ) - - if (-not $Domains -or $Domains.Count -eq 0) { - return $false - } - - foreach ($domain in $Domains) { - if (Test-DomainMatch -DomainHost $DomainHost -RuleDomain $domain) { - return $true - } - } - - return $false -} - -function Test-DlpRuleTimeWindow { - param( - [int]$CurrentHour, - [AllowNull()][int]$HourFrom, - [AllowNull()][int]$HourTo - ) - - if ($null -eq $HourFrom -or $null -eq $HourTo) { - return $true - } - - if ($HourFrom -eq $HourTo) { - return $true - } - - if ($HourFrom -lt $HourTo) { - return ($CurrentHour -ge $HourFrom -and $CurrentHour -lt $HourTo) - } - - return ($CurrentHour -ge $HourFrom -or $CurrentHour -lt $HourTo) -} - -function Load-DlpPolicy { - param([string]$Path) - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-CollectorLog ("DLP-политика не найдена, DLP отключен: {0}" -f $Path) - return - } - - try { - $parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - $defaults = $parsed.defaults - if ($defaults) { - if ($defaults.PSObject.Properties.Name -contains 'enabled') { - $script:DlpDefaults.enabled = [bool]$defaults.enabled - } - if ($defaults.cooldownSeconds) { - $script:DlpDefaults.cooldownSeconds = [int]$defaults.cooldownSeconds - } - if ($defaults.action) { - $script:DlpDefaults.action = [string]$defaults.action - } - if ($defaults.severity) { - $script:DlpDefaults.severity = [string]$defaults.severity - } - } - - $loaded = @() - foreach ($rule in @($parsed.rules)) { - if (-not $rule) { continue } - $when = $rule.when - if (-not $when) { - $when = [pscustomobject]@{} - } - $loaded += [pscustomobject]@{ - id = [string]$rule.id - enabled = if ($rule.PSObject.Properties.Name -contains 'enabled') { [bool]$rule.enabled } else { $true } - action = if ($rule.action) { [string]$rule.action } else { [string]$script:DlpDefaults.action } - severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:DlpDefaults.severity } - message = if ($rule.message) { [string]$rule.message } else { "Сработало DLP-правило: $($rule.id)" } - cooldownSeconds = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:DlpDefaults.cooldownSeconds } - when = [pscustomobject]@{ - domains = if ($when.PSObject.Properties.Name -contains 'domains') { @($when.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categoryGroups = if ($when.PSObject.Properties.Name -contains 'categoryGroups') { @($when.categoryGroups | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - categories = if ($when.PSObject.Properties.Name -contains 'categories') { @($when.categories | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - browsers = if ($when.PSObject.Properties.Name -contains 'browsers') { @($when.browsers | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ }) } else { @() } - urlRegex = if ($when.PSObject.Properties.Name -contains 'urlRegex' -and $when.urlRegex) { [string]$when.urlRegex } else { $null } - titleRegex = if ($when.PSObject.Properties.Name -contains 'titleRegex' -and $when.titleRegex) { [string]$when.titleRegex } else { $null } - hourFrom = if ($when.PSObject.Properties.Name -contains 'hourFrom') { [int]$when.hourFrom } else { $null } - hourTo = if ($when.PSObject.Properties.Name -contains 'hourTo') { [int]$when.hourTo } else { $null } - } - } - } - - $script:DlpRules = @($loaded) - Write-CollectorLog ("DLP-политика загружена: включена={0}, правил={1}" -f $script:DlpDefaults.enabled, $script:DlpRules.Count) - } - catch { - Write-CollectorLog ("не удалось разобрать DLP-политику: {0}" -f $_.Exception.Message) - } -} - -function Test-DlpRuleMatch { - param( - [pscustomobject]$Rule, - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $Rule.enabled) { - return $false - } - - $when = $Rule.when - $currentHour = (Get-Date).Hour - if (-not (Test-DlpRuleTimeWindow -CurrentHour $currentHour -HourFrom $when.hourFrom -HourTo $when.hourTo)) { - return $false - } - - if ($when.domains.Count -gt 0) { - $domainMatched = (Test-DomainListMatch -DomainHost $Domain -Domains $when.domains) -or (Test-DomainListMatch -DomainHost $RootDomain -Domains $when.domains) - if (-not $domainMatched) { - return $false - } - } - - if ($when.categoryGroups.Count -gt 0 -and ($when.categoryGroups -notcontains $CategoryGroup.ToLowerInvariant())) { - return $false - } - - if ($when.categories.Count -gt 0 -and ($when.categories -notcontains $Category.ToLowerInvariant())) { - return $false - } - - if ($when.browsers.Count -gt 0 -and ($when.browsers -notcontains $BrowserKey.ToLowerInvariant())) { - return $false - } - - if ($when.urlRegex) { - if (-not ($Url -match $when.urlRegex)) { - return $false - } - } - - if ($when.titleRegex) { - if (-not ($Title -match $when.titleRegex)) { - return $false - } - } - - return $true -} - -function Get-DlpDecision { - param( - [string]$Domain, - [string]$RootDomain, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$Category, - [string]$CategoryGroup - ) - - if (-not $script:DlpDefaults.enabled) { - return $null - } - - foreach ($rule in $script:DlpRules) { - if (Test-DlpRuleMatch -Rule $rule -Domain $Domain -RootDomain $RootDomain -Url $Url -Title $Title -BrowserKey $BrowserKey -Category $Category -CategoryGroup $CategoryGroup) { - return $rule - } - } - - return $null -} - -function Should-EmitIncident { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:IncidentState.ContainsKey($Fingerprint)) { - $last = [datetime]$script:IncidentState[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:IncidentState[$Fingerprint] = $now - return $true -} - -function Send-DlpIncidentHeartbeat { - param( - [pscustomobject]$Decision, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId ([string]$Decision.id) -SignalType 'web' - } - catch { - } - } - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = [string]$Decision.id - action = [string]$Decision.action - severity = [string]$Decision.severity - message = [string]$Decision.message - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - username = $env:USERNAME - hostname = $script:Hostname - sessionId = $script:SessionId - source = 'uia-native-dlp' - } + $captureData - } | ConvertTo-Json -Depth 5 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-CollectorLog ("не удалось сделать снимок инцидента: {0}" -f $_.Exception.Message) - return @{} - } -} - -function Get-ForegroundWindowContext { - $handle = [NativeAwMethods]::GetForegroundWindow() - if ($handle -eq [IntPtr]::Zero) { - return $null - } - - $processId = [uint32]0 - [void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId) - if (-not $processId) { - return $null - } - - $process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue - if (-not $process) { - return $null - } - - $textLength = [NativeAwMethods]::GetWindowTextLength($handle) - $builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260)) - [void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity) - - return [pscustomobject]@{ - Handle = $handle - ProcessName = $process.ProcessName.ToLowerInvariant() - Title = $builder.ToString() - } -} - -function Get-BrowserUrlFromWindow { - param([IntPtr]$Handle) - - $root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle) - if (-not $root) { - return $null - } - - $editCondition = [System.Windows.Automation.PropertyCondition]::new( - [System.Windows.Automation.AutomationElement]::ControlTypeProperty, - [System.Windows.Automation.ControlType]::Edit - ) - - $edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition) - foreach ($edit in $edits) { - $valuePattern = $null - if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) { - $candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value - if ($candidate) { - return $candidate - } - } - - $candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name - if ($candidateFromName) { - return $candidateFromName - } - } - - return $null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType = 'web.tab.current' - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - try { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - $script:KnownBuckets[$BucketId] = $true - return - } - catch { - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-Heartbeat { - param( - [string]$BucketId, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName - ) - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Send-CategoryHeartbeat { - param( - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup, - [string]$CategoryRule - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - categoryRule = $CategoryRule - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -Load-CustomCategoryRules -Path $resolvedRulesPath -Load-DlpPolicy -Path $resolvedPolicyPath -Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) - -while ($true) { - try { - $context = Get-ForegroundWindowContext - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } - - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } - - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } - } - } - } - } - } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - try { - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json; charset=utf-8' -Body ([Text.Encoding]::UTF8.GetBytes($body)) | Out-Null - } - catch { - Invoke-RestMethod -Method Get -Uri "$($script:ApiBase)/buckets/$BucketId" | Out-Null - } - $script:KnownBuckets[$BucketId] = $true -} - -function Send-Heartbeat { - param( - [string]$BucketId, - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName - ) - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Send-CategoryHeartbeat { - param( - [string]$Url, - [string]$Title, - [string]$BrowserKey, - [string]$ProcessName, - [string]$Domain, - [string]$RootDomain, - [string]$Category, - [string]$CategoryGroup, - [string]$CategoryRule - ) - - $bucketId = 'aw-detmir-web-category_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-detmir-web-category' -BucketType 'aw.web.category' - - $event = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - url = $Url - title = $Title - browser = $BrowserKey - app = "$ProcessName.exe" - domain = $Domain - rootDomain = $RootDomain - category = $Category - categoryGroup = $CategoryGroup - categoryRule = $CategoryRule - source = 'uia-native' - sessionId = $script:SessionId - } - } | ConvertTo-Json -Depth 4 -Compress - - Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event -TimeoutSec 15 -DisableKeepAlive | Out-Null + Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null } Load-CustomCategoryRules -Path $resolvedRulesPath diff --git a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 index 8a83b16..92e5494 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -112,8 +112,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output 'ActivityWatch развёрнут для пользователей:' -$targetUsers | ForEach-Object { Write-Output " - $_" } -Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Output "Каталог данных: $StateRoot" -Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)" +Write-Host 'ActivityWatch развёрнут для пользователей:' +$targetUsers | ForEach-Object { Write-Host " - $_" } +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 index 1efc953..fa6fd75 100644 --- a/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -136,6 +136,6 @@ if ($reportDirectory) { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 -Write-Output 'Комплексное развёртывание ActivityWatch завершено.' -Write-Output "Пользователи: $($resolvedUsers -join ', ')" -Write-Output "Отчёт: $effectiveReportPath" +Write-Host 'Комплексное развёртывание ActivityWatch завершено.' +Write-Host "Пользователи: $($resolvedUsers -join ', ')" +Write-Host "Отчёт: $effectiveReportPath" diff --git a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 index 7f4952a..160265d 100755 --- a/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 +++ b/install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -104,9 +104,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser" -Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Output "Каталог установки: $InstallRoot" -Write-Output "Каталог данных: $StateRoot" -Write-Output "Файл правил: $($assetResult.ActiveRules)" -Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)" +Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог установки: $InstallRoot" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл правил: $($assetResult.ActiveRules)" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 index 5a09101..b286e48 100644 --- a/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 @@ -1,35 +1,4 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { Write-Error [CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -71,7 +40,7 @@ function Invoke-AwJsonPost { ) $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null + Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null } function Ensure-Bucket { @@ -352,44 +321,6 @@ function Get-StringHash { } } -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - function Load-DlpPolicy { param([string]$Path) @@ -852,7 +783,7 @@ while ($true) { } try { - $clipboardText = Get-ClipboardTextSafe + $clipboardText = Get-Clipboard -Raw -ErrorAction SilentlyContinue if ($clipboardText) { $clipboardHash = Get-StringHash -Value $clipboardText if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { @@ -988,6678 +919,3 @@ while ($true) { Start-Sleep -Seconds $resolvedPollSeconds } -; } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} diff --git a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 index 1c13665..d382f41 100755 --- a/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +++ b/install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -151,6 +151,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output 'Укрепление и восстановление ActivityWatch завершены.' -Write-Output "Конфигурация: $effectiveConfigPath" -Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')" +Write-Host 'Укрепление и восстановление ActivityWatch завершены.' +Write-Host "Конфигурация: $effectiveConfigPath" +Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')" diff --git a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 index f91e6eb..eb7cbe4 100644 --- a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 +++ b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding(SupportsShouldProcess = $true)] +[CmdletBinding(SupportsShouldProcess = $true)] param( [string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2', [string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2', @@ -154,36 +154,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W @{ Source = $NewStateRoot; Name = 'new-state' } )) { if (Test-Path -LiteralPath $item.Source) { - $backupDest = Join-Path $backupRoot $item.Name - New-ActivityWatchDirectory -Path $backupDest - - $excludeDirs = @() - if ($item.Source -eq $NewStateRoot) { - # Avoid infinite recursion: backupRoot is inside NewStateRoot by default. - $excludeDirs += $backupRoot - } - - $robocopyArgs = @( - $item.Source, - $backupDest, - '/E', - '/R:1', - '/W:1', - '/NFL', - '/NDL', - '/NJH', - '/NJS', - '/NP' - ) - if ($excludeDirs.Count -gt 0) { - $robocopyArgs += '/XD' - $robocopyArgs += $excludeDirs - } - - & robocopy @robocopyArgs | Out-Null - if ($LASTEXITCODE -ge 8) { - throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'" - } + Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force } } diff --git a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 index 15a4822..52b35d2 100644 --- a/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 +++ b/install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json' + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json' ) Set-StrictMode -Version Latest @@ -13,26 +13,49 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath $installRoot = [string]$config.paths.installRoot $stateRoot = [string]$config.paths.stateRoot $collectorScript = [string]$config.paths.collectorScript +$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' } +$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } $rulesPath = [string]$config.paths.rulesPath +$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' } $launchScript = [string]$config.paths.launchScript $recoveryScript = [string]$config.paths.recoveryScript +$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true } +$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true } $requiredFiles = @( - (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'), - (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), $collectorScript, + $endpointCollectorScript, + $sessionCollectorScript, $rulesPath, + $policyPath, $launchScript, $recoveryScript, $ConfigPath ) +if ($afkExpected) { + $requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe') +} +if ($windowExpected) { + $requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe') +} $missingFiles = @( $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) } ) -$processNames = @('aw-watcher-afk', 'aw-watcher-window') -$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId +$processNames = @() +if ($afkExpected) { $processNames += 'aw-watcher-afk' } +if ($windowExpected) { $processNames += 'aw-watcher-window' } +$runningProcesses = @() +if ($processNames.Count -gt 0) { + $runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId +} +$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and + $_.CommandLine -match [Regex]::Escape($sessionCollectorScript) + } | + Select-Object Name, ProcessId, SessionId, CommandLine $taskNames = @() if ($config.userTasks) { @@ -42,7 +65,7 @@ $taskNames += [string]$config.recovery.taskName $taskNames = $taskNames | Sort-Object -Unique $tasks = foreach ($taskName in $taskNames) { - $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 if ($task) { [pscustomobject]@{ taskName = $task.TaskName @@ -53,7 +76,7 @@ $tasks = foreach ($taskName in $taskNames) { else { [pscustomobject]@{ taskName = $taskName - state = 'Missing' + state = 'Отсутствует' present = $false } } @@ -76,8 +99,16 @@ $result = [ordered]@{ ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present })) } processes = [ordered]@{ + expected = $processNames list = @($runningProcesses) - ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) + sessionCollectors = @($sessionCollectorProcesses) + ok = [bool]( + ( + ($processNames.Count -eq 0) -or + (($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count) + ) -and + ($sessionCollectorProcesses.Count -ge 1) + ) } } diff --git a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 index e3021c5..226a500 100644 --- a/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 +++ b/install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 @@ -1,44 +1,4 @@ -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { Write-Error param( +param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$Hostname, [int]$PollSeconds = 30 @@ -87,12 +47,7 @@ function Ensure-Bucket { hostname = $HostnameValue } | ConvertTo-Json -Compress - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } + Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body } function Get-SessionRecords { @@ -204,458 +159,3 @@ while ($true) { Start-Sleep -Seconds $sleepSec } -; } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { Write-Error param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { - } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { - } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { - } - } - - Start-Sleep -Seconds $sleepSec -} -; } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { Write-Error param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { - } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { - } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { - } - } - - Start-Sleep -Seconds $sleepSec -} -; } - } - - Start-Sleep -Seconds $sleepSec -} diff --git a/proxmox/create-ct.sh b/proxmox/create-ct.sh index 1d2fe59..c92ef25 100755 --- a/proxmox/create-ct.sh +++ b/proxmox/create-ct.sh @@ -1,5 +1,4 @@ #!/bin/sh -# shellcheck disable=SC1007 set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) diff --git a/proxmox/push-aw-artifacts.sh b/proxmox/push-aw-artifacts.sh index 0db6733..d3b2d91 100755 --- a/proxmox/push-aw-artifacts.sh +++ b/proxmox/push-aw-artifacts.sh @@ -1,5 +1,4 @@ #!/bin/sh -# shellcheck disable=SC1007 set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) diff --git a/scripts/aggregate_dlp_events.py b/scripts/aggregate_dlp_events.py index dcc58cd..7be7760 100755 --- a/scripts/aggregate_dlp_events.py +++ b/scripts/aggregate_dlp_events.py @@ -18,12 +18,7 @@ JsonScalar: TypeAlias = str | int | float | bool | None JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"] -DEFAULT_BUCKET_PREFIXES = ( - "aw-file-operations_", - "aw-dlp-incidents_", - "aw-dlp-endpoint-signals_", - "aw-email-monitor_", -) +DEFAULT_BUCKET_PREFIXES = ("aw-file-operations_", "aw-dlp-incidents_") DEFAULT_SQLITE_PATH = "data/dlp-events.sqlite3" EVENT_COLUMNS = ( "bucket_id", @@ -139,10 +134,6 @@ def bucket_stream_type(bucket: Bucket) -> str | None: return "file_operation" if bucket.id.startswith("aw-dlp-incidents_") or bucket.type == "aw.dlp.incident": return "dlp_incident" - if bucket.id.startswith("aw-dlp-endpoint-signals_") or bucket.type == "aw.dlp.endpoint.signal": - return "dlp_endpoint_signal" - if bucket.id.startswith("aw-email-monitor_") or bucket.type == "aw.email.signal": - return "email_monitor" return None diff --git a/scripts/check_install_kit_vs_repo.sh b/scripts/check_install_kit_vs_repo.sh index bda69ec..97d165d 100755 --- a/scripts/check_install_kit_vs_repo.sh +++ b/scripts/check_install_kit_vs_repo.sh @@ -4,26 +4,11 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" -# shellcheck disable=SC2034 KIT_DIR="install-kit-awindows-20260427-211240" -PY_BIN="${PY_BIN:-}" -if [[ -z "$PY_BIN" ]]; then - if command -v python3 >/dev/null 2>&1; then - PY_BIN="python3" - elif command -v python >/dev/null 2>&1; then - PY_BIN="python" - else - echo "ERROR: python3/python not found" - exit 127 - fi -fi - -"$PY_BIN" - <<'PY' +python - <<'PY' from pathlib import Path import hashlib -import os -import sys root=Path('.') kit=Path('install-kit-awindows-20260427-211240') @@ -39,13 +24,6 @@ missing_in_repo=[] for kp in sorted(p for p in kit.rglob('*') if p.is_file() and p.name!='MANIFEST.txt'): rel=kp.relative_to(kit) - rel_s=str(rel) - if rel_s.startswith("server-configs-192.168.100.21/"): - continue - if rel_s == "README-INSTALL-KIT.txt": - continue - if "__pycache__" in kp.parts or kp.suffix == ".pyc": - continue rp=root/rel if not rp.exists(): missing_in_repo.append(str(rel)) @@ -72,9 +50,4 @@ if ps_mismatches: print('--- PowerShell mismatches ---') for p in ps_mismatches: print(p) - -strict = os.getenv("ALLOW_KIT_DRIFT", "").lower() not in {"1", "true", "yes"} -if strict and (missing_in_repo or mismatches): - print("ERROR: install-kit drift detected. Set ALLOW_KIT_DRIFT=1 to bypass.") - sys.exit(1) PY diff --git a/scripts/install_aw_linux_remote_worker.sh b/scripts/install_aw_linux_remote_worker.sh index 5ee46d9..3cebd20 100644 --- a/scripts/install_aw_linux_remote_worker.sh +++ b/scripts/install_aw_linux_remote_worker.sh @@ -49,7 +49,6 @@ while [ "$#" -gt 0 ]; do esac done -# shellcheck disable=SC1007 SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \ diff --git a/scripts/prod_rollout.sh b/scripts/prod_rollout.sh index 1907e29..1f9ebfd 100644 --- a/scripts/prod_rollout.sh +++ b/scripts/prod_rollout.sh @@ -21,11 +21,9 @@ prompt_secret() { if [[ -n "${!var_name:-}" ]]; then return 0 fi - local _val - read -r -s -p "${prompt}: " _val + read -r -s -p "${prompt}: " "$var_name" echo - printf -v "$var_name" '%s' "$_val" - declare -gx "$var_name" + export "$var_name" } require_cmd git @@ -35,12 +33,6 @@ require_cmd ansible log "Repo: ${ROOT_DIR}" log "Branch: $(git branch --show-current)" -if [[ "${AW_MAINTENANCE_ACK:-}" != "YES" ]]; then - log "ERROR: maintenance window is required." - log "Set AW_MAINTENANCE_ACK=YES to proceed." - exit 4 -fi - log "Running local quality gate..." ./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log" @@ -73,10 +65,6 @@ log "Preflight connectivity..." ansible -i ansible/inventory.ini aw_server -m ping | tee -a "${LOG_DIR}/ping_aw_server.log" ansible -i ansible/inventory.ini aw_windows -m win_ping | tee -a "${LOG_DIR}/ping_aw_windows.log" -log "Preflight ActivityWatch API/data checks..." -./check-aw-data.sh | tee -a "${LOG_DIR}/check_aw_data.log" -./check-aw-full.sh | tee -a "${LOG_DIR}/check_aw_full.log" - log "Dry-run aw_server..." ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --check --diff | tee -a "${LOG_DIR}/check_aw_server.log" diff --git a/scripts/quality-gate.sh b/scripts/quality-gate.sh index abcfc23..3b88862 100755 --- a/scripts/quality-gate.sh +++ b/scripts/quality-gate.sh @@ -4,42 +4,33 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT_DIR" -echo "[1/6] Bash syntax check" +echo "[1/4] Bash syntax check" find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n -echo "[2/6] Shellcheck (if available)" +echo "[2/4] Shellcheck (if available)" if command -v shellcheck >/dev/null 2>&1; then find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016 else echo "shellcheck not found, skipping." fi -echo "[3/6] PowerShell parse check (if pwsh available)" +echo "[3/4] PowerShell parse check (if pwsh available)" if command -v pwsh >/dev/null 2>&1; then - if ! pwsh -NoLogo -NoProfile -Command ' + pwsh -NoLogo -NoProfile -Command ' $ErrorActionPreference = "Stop" Get-ChildItem windows -Filter *.ps1 | ForEach-Object { [void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$null) } [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null) [void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null) - '; then - echo "pwsh parse check failed due runtime environment; skipping." - fi + ' else echo "pwsh not found, skipping." fi -echo "[4/6] Install-kit consistency check" -./scripts/check_install_kit_vs_repo.sh -echo "[5/6] Generated-artifacts guard" -if git status --short | grep -E '^(\\?\\?| M|M ) (\\.graphify_|graphify-out/|reports/|tmp/|data/)'; then - echo "ERROR: generated artifacts detected in working tree. Clean or ignore them before rollout." - exit 1 -fi -echo "[6/6] Ansible syntax check (if ansible-playbook available)" +echo "[4/4] Ansible syntax check (if ansible-playbook available)" if command -v ansible-playbook >/dev/null 2>&1; then for playbook in ansible/*.yml; do ansible-playbook --syntax-check "$playbook" -i ansible/inventory.example.ini >/dev/null diff --git a/scripts/validate_install_kit.sh b/scripts/validate_install_kit.sh index 3761820..f0ddc43 100755 --- a/scripts/validate_install_kit.sh +++ b/scripts/validate_install_kit.sh @@ -6,10 +6,7 @@ cd "$ROOT_DIR" KIT_DIR="install-kit-awindows-20260427-211240" MANIFEST="$KIT_DIR/MANIFEST.txt" -# ZIP/TAR variables are declared for archive checks in this script; keep them for clarity -# shellcheck disable=SC2034 ZIP_ARCHIVE="install-kit-awindows-20260427-211240.zip" -# shellcheck disable=SC2034 TAR_ARCHIVE="install-kit-awindows-20260427-211240.tar.gz" required_files=( diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index af4e0c6..5f1e7fb 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -376,7 +376,6 @@ function New-ActivityWatchDeploymentConfig { [string]$LaunchScriptPath, [Parameter(Mandatory = $true)] [string]$RecoveryScriptPath, - [string]$AwHostname, [Parameter(Mandatory = $true)] [pscustomobject[]]$UserTasks, [string]$PackageVersion = 'v0.13.2' @@ -387,7 +386,6 @@ function New-ActivityWatchDeploymentConfig { return [pscustomobject]@{ version = 1 generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - awHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname } server = [pscustomobject]@{ host = $ServerHost port = $ServerPort @@ -744,7 +742,7 @@ function Start-CollectorScriptIfNeeded { `$installRoot = [string]`$config.paths.installRoot `$stateRoot = [string]`$config.paths.stateRoot `$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port -`$script:Hostname = if (`$config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]`$config.awHostname)) { [string]`$config.awHostname } else { `$env:COMPUTERNAME } +`$script:Hostname = `$env:COMPUTERNAME `$script:KnownBuckets = @{} `$collectorScript = [string]`$config.paths.collectorScript `$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { Join-Path `$stateRoot 'dlp-endpoint-signals-collector.ps1' } diff --git a/windows/aw-standalone-service.ps1 b/windows/aw-standalone-service.ps1 deleted file mode 100644 index a2cc659..0000000 --- a/windows/aw-standalone-service.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -[CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [int]$LoopSeconds = 20 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - throw "Config not found: $Path" - } - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Write-ServiceLog { - param([string]$Message) - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch {} -} - -function Start-CollectorIfNeeded { - param( - [string]$ScriptPath, - [string]$ConfigPath - ) - - if ([string]::IsNullOrWhiteSpace($ScriptPath) -or -not (Test-Path -LiteralPath $ScriptPath)) { - return - } - - $escaped = [Regex]::Escape($ScriptPath) - $running = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - $_.Name -eq 'powershell.exe' -and - $_.CommandLine -match $escaped -and - $_.CommandLine -match [Regex]::Escape($ConfigPath) - } | - Select-Object -First 1 - - if ($running) { - return - } - - $args = @('-NoProfile', '-ExecutionPolicy', 'Bypass') - if ($ScriptPath -like '*dlp-endpoint-signals*') { - $args += '-STA' - } - $args += @('-File', $ScriptPath, '-ConfigPath', $ConfigPath) - Start-Process -FilePath 'powershell.exe' -ArgumentList $args -WindowStyle Hidden | Out-Null - Write-ServiceLog ("started collector: {0}" -f $ScriptPath) -} - -$cfg = Get-Config -Path $ConfigPath -$stateRoot = if ($cfg.paths -and $cfg.paths.stateRoot) { [string]$cfg.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' } -$logsRoot = Join-Path $stateRoot 'logs' -if (-not (Test-Path -LiteralPath $logsRoot)) { - New-Item -Path $logsRoot -ItemType Directory -Force | Out-Null -} -$script:LogPath = Join-Path $logsRoot 'standalone-agent-service.log' - -Write-ServiceLog ('service loop started, config={0}' -f $ConfigPath) - -while ($true) { - try { - $cfg = Get-Config -Path $ConfigPath - $paths = $cfg.paths - - Start-CollectorIfNeeded -ScriptPath ([string]$paths.collectorScript) -ConfigPath $ConfigPath - Start-CollectorIfNeeded -ScriptPath ([string]$paths.endpointCollectorScript) -ConfigPath $ConfigPath - Start-CollectorIfNeeded -ScriptPath ([string]$paths.fileCollectorScript) -ConfigPath $ConfigPath - if ($paths.PSObject.Properties.Name -contains 'emailCollectorScript') { - Start-CollectorIfNeeded -ScriptPath ([string]$paths.emailCollectorScript) -ConfigPath $ConfigPath - } - if ($paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { - Start-CollectorIfNeeded -ScriptPath ([string]$paths.sessionCollectorScript) -ConfigPath $ConfigPath - } - } - catch { - Write-ServiceLog ("loop error: {0}" -f $_.Exception.Message) - } - Start-Sleep -Seconds ([Math]::Max($LoopSeconds, 5)) -} - diff --git a/windows/browser-domains-native-collector.ps1 b/windows/browser-domains-native-collector.ps1 index 8b7ad88..f4eff79 100755 --- a/windows/browser-domains-native-collector.ps1 +++ b/windows/browser-domains-native-collector.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -59,18 +59,16 @@ $resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $P $resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } $resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) } $resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) } -$resolvedHealthPath = Join-Path $resolvedLogsRoot ("health-browser-domains-{0}.json" -f $env:USERNAME) $resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } $resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } $resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } -$resolvedHostname = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$deploymentConfig.awHostname)) { [string]$deploymentConfig.awHostname } else { [string]$env:COMPUTERNAME } if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null } $script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $resolvedHostname +$script:Hostname = $env:COMPUTERNAME $script:SessionId = (Get-Process -Id $PID).SessionId $script:KnownBuckets = @{} $script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled @@ -87,7 +85,6 @@ $script:DlpDefaults = [ordered]@{ action = 'log' severity = 'low' } -$script:HealthPath = $resolvedHealthPath $script:BrowserMap = @{ msedge = 'edge' chrome = 'chrome' @@ -137,23 +134,6 @@ function Write-DlpIncidentLog { } } -function Write-CollectorHealth { - param([string]$Status = 'running') - try { - $health = @{ - collector = 'browser-domains-native' - hostname = $script:Hostname - sessionId = $script:SessionId - status = $Status - apiBase = $script:ApiBase - ts = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Depth 4 - Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8 - } - catch { - } -} - function Test-DomainMatch { param( [string]$DomainHost, @@ -810,52 +790,46 @@ Load-CustomCategoryRules -Path $resolvedRulesPath Load-DlpPolicy -Path $resolvedPolicyPath Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase) -try { - while ($true) { - try { - Write-CollectorHealth -Status 'running' - $context = Get-ForegroundWindowContext - if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { - $url = Get-BrowserUrlFromWindow -Handle $context.Handle - if ($url) { - $browserKey = $script:BrowserMap[$context.ProcessName] - $domain = Get-HostFromUrl -Url $url - if (-not $domain) { - $domain = 'unknown' - } +while ($true) { + try { + $context = Get-ForegroundWindowContext + if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) { + $url = Get-BrowserUrlFromWindow -Handle $context.Handle + if ($url) { + $browserKey = $script:BrowserMap[$context.ProcessName] + $domain = Get-HostFromUrl -Url $url + if (-not $domain) { + $domain = 'unknown' + } - $rootDomain = Get-RootDomain -DomainHost $domain - if (-not $rootDomain) { - $rootDomain = $domain - } + $rootDomain = Get-RootDomain -DomainHost $domain + if (-not $rootDomain) { + $rootDomain = $domain + } - $category = Get-WebCategory -DomainHost $domain - $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) - Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName - Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule + $category = Get-WebCategory -DomainHost $domain + $bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname + Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey) + Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName + Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule - $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group - if ($decision) { - $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME - $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) - if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { - Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) - if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { - Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group - } + $decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group + if ($decision) { + $fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME + $cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30) + if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) { + Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url) + if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) { + Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group } } } } } - catch { - Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds } -} -finally { - Write-CollectorHealth -Status 'stopped' + catch { + Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message) + } + + Start-Sleep -Seconds $resolvedPollSeconds } diff --git a/windows/deploy-domain-users.ps1 b/windows/deploy-domain-users.ps1 index 7b88f80..92e5494 100755 --- a/windows/deploy-domain-users.ps1 +++ b/windows/deploy-domain-users.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -24,7 +24,6 @@ param( [bool]$IncidentScreenshotEnabled = $true, [string]$IncidentArtifactsRoot, [bool]$LogonMarkerEnabled = $true, - [string]$AwHostname, [string]$CustomRulesPath, [string]$CustomPolicyPath ) @@ -101,7 +100,6 @@ $config = New-ActivityWatchDeploymentConfig ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentArtifactsRoot $IncidentArtifactsRoot ` -LogonMarkerEnabled $LogonMarkerEnabled ` - -AwHostname $AwHostname ` -LaunchScriptPath $launchScriptPath ` -RecoveryScriptPath $recoveryScriptPath ` -UserTasks $taskDefinitions ` @@ -114,8 +112,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output 'ActivityWatch развёрнут для пользователей:' -$targetUsers | ForEach-Object { Write-Output " - $_" } -Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Output "Каталог данных: $StateRoot" -Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)" +Write-Host 'ActivityWatch развёрнут для пользователей:' +$targetUsers | ForEach-Object { Write-Host " - $_" } +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 index ee8d255..fa6fd75 100644 --- a/windows/deploy-ensemble.ps1 +++ b/windows/deploy-ensemble.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -24,7 +24,6 @@ param( [bool]$IncidentScreenshotEnabled = $true, [string]$IncidentArtifactsRoot, [bool]$LogonMarkerEnabled = $true, - [string]$AwHostname, [string]$CustomRulesPath, [string]$CustomPolicyPath, [string]$ReportPath, @@ -72,7 +71,6 @@ if (-not (Test-Path -LiteralPath $deployScript)) { -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentArtifactsRoot $IncidentArtifactsRoot ` -LogonMarkerEnabled $LogonMarkerEnabled ` - -AwHostname $AwHostname ` -CustomRulesPath $CustomRulesPath ` -CustomPolicyPath $CustomPolicyPath @@ -96,7 +94,6 @@ if (-not $SkipHardening) { -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentArtifactsRoot $IncidentArtifactsRoot ` -LogonMarkerEnabled $LogonMarkerEnabled ` - -AwHostname $AwHostname ` -CustomRulesPath $CustomRulesPath ` -CustomPolicyPath $CustomPolicyPath } @@ -139,6 +136,6 @@ if ($reportDirectory) { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 -Write-Output 'Комплексное развёртывание ActivityWatch завершено.' -Write-Output "Пользователи: $($resolvedUsers -join ', ')" -Write-Output "Отчёт: $effectiveReportPath" +Write-Host 'Комплексное развёртывание ActivityWatch завершено.' +Write-Host "Пользователи: $($resolvedUsers -join ', ')" +Write-Host "Отчёт: $effectiveReportPath" diff --git a/windows/deploy-single-user.ps1 b/windows/deploy-single-user.ps1 index 1f5400c..160265d 100755 --- a/windows/deploy-single-user.ps1 +++ b/windows/deploy-single-user.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$ServerHost, @@ -22,7 +22,6 @@ param( [bool]$IncidentScreenshotEnabled = $true, [string]$IncidentArtifactsRoot, [bool]$LogonMarkerEnabled = $true, - [string]$AwHostname, [string]$CustomRulesPath, [string]$CustomPolicyPath ) @@ -93,7 +92,6 @@ $config = New-ActivityWatchDeploymentConfig ` -IncidentScreenshotEnabled $IncidentScreenshotEnabled ` -IncidentArtifactsRoot $IncidentArtifactsRoot ` -LogonMarkerEnabled $LogonMarkerEnabled ` - -AwHostname $AwHostname ` -LaunchScriptPath $launchScriptPath ` -RecoveryScriptPath $recoveryScriptPath ` -UserTasks $taskDefinitions ` @@ -106,9 +104,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser" -Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" -Write-Output "Каталог установки: $InstallRoot" -Write-Output "Каталог данных: $StateRoot" -Write-Output "Файл правил: $($assetResult.ActiveRules)" -Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)" +Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser" +Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort" +Write-Host "Каталог установки: $InstallRoot" +Write-Host "Каталог данных: $StateRoot" +Write-Host "Файл правил: $($assetResult.ActiveRules)" +Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)" diff --git a/windows/dlp-endpoint-signals-collector.ps1 b/windows/dlp-endpoint-signals-collector.ps1 index e864191..0b2f810 100644 --- a/windows/dlp-endpoint-signals-collector.ps1 +++ b/windows/dlp-endpoint-signals-collector.ps1 @@ -21,1073 +21,6 @@ function Get-DeploymentConfig { return $null } -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } -$resolvedHostname = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$deploymentConfig.awHostname)) { [string]$deploymentConfig.awHostname } else { [string]$env:COMPUTERNAME } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $resolvedHostname -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - function Write-EndpointLog { param([string]$Message) if (-not $script:LocalAgentLogsEnabled) { @@ -2024,5643 +957,3 @@ while ($true) { Start-Sleep -Seconds $resolvedPollSeconds } -; } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { Write-Error [CmdletBinding()] -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$ServerHost, - [int]$ServerPort, - [ValidateSet('http', 'https')] - [string]$ServerScheme, - [string]$PolicyPath, - [string]$LogPath, - [int]$PollSeconds -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-DeploymentConfig { - param([string]$Path) - if ($Path -and (Test-Path -LiteralPath $Path)) { - return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - } - return $null -} - -function Write-EndpointLog { - param([string]$Message) - if (-not $script:LocalAgentLogsEnabled) { - return - } - try { - Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message) - } - catch { - } -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -TimeoutSec 15 -DisableKeepAlive | Out-Null -} - -function Ensure-Bucket { - param( - [string]$BucketId, - [string]$ClientName, - [string]$BucketType - ) - - if ($script:KnownBuckets.ContainsKey($BucketId)) { - return - } - - $body = @{ - client = $ClientName - type = $BucketType - hostname = $script:Hostname - } | ConvertTo-Json -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body - $script:KnownBuckets[$BucketId] = $true -} - -function Send-EndpointSignalHeartbeat { - param( - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-endpoint-signals_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-endpoint-signals' -BucketType 'aw.dlp.endpoint.signal' - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data - } | ConvertTo-Json -Depth 6 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Send-DlpIncidentHeartbeat { - param( - [string]$RuleId, - [string]$Action, - [string]$Severity, - [string]$Message, - [string]$SignalType, - [hashtable]$Data - ) - - $bucketId = 'aw-dlp-incidents_' + $script:Hostname - Ensure-Bucket -BucketId $bucketId -ClientName 'aw-dlp-incidents' -BucketType 'aw.dlp.incident' - - $captureData = @{} - if ($script:IncidentScreenshotEnabled) { - try { - $captureData = Capture-IncidentScreenshot -RuleId $RuleId -SignalType $SignalType - } - catch { - } - } - - $payload = @{ - timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - duration = 0 - data = @{ - ruleId = $RuleId - action = $Action - severity = $Severity - message = $Message - signalType = $SignalType - username = $env:USERNAME - sessionId = $script:SessionId - hostname = $script:Hostname - source = 'endpoint-signals-phase2' - } + $Data + $captureData - } | ConvertTo-Json -Depth 7 -Compress - - Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload -} - -function Get-FileSha256Hex { - param([Parameter(Mandatory = $true)][string]$Path) - try { - $sha = [Security.Cryptography.SHA256]::Create() - $stream = [IO.File]::OpenRead($Path) - try { - ($sha.ComputeHash($stream) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $stream.Dispose() - $sha.Dispose() - } - } - catch { - return $null - } -} - -function Ensure-Directory { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -function Get-IncidentScreenshotPath { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - $safeUser = ($env:USERNAME -replace '[^A-Za-z0-9_.-]', '_') - $safeRule = ($RuleId -replace '[^A-Za-z0-9_.-]', '_') - $safeType = ($SignalType -replace '[^A-Za-z0-9_.-]', '_') - $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd_HHmmss_fff') - $file = '{0}_{1}_sid{2}_{3}_{4}.png' -f $script:Hostname, $safeUser, $script:SessionId, $safeType, $safeRule - $file = '{0}_{1}' -f $stamp, $file - return (Join-Path $script:IncidentArtifactsRoot $file) -} - -function Ensure-ScreenshotTypesLoaded { - if ($script:ScreenshotTypesLoaded) { - return - } - Add-Type -AssemblyName System.Windows.Forms | Out-Null - Add-Type -AssemblyName System.Drawing | Out-Null - $script:ScreenshotTypesLoaded = $true -} - -function Capture-IncidentScreenshot { - param( - [Parameter(Mandatory = $true)][string]$RuleId, - [Parameter(Mandatory = $true)][string]$SignalType - ) - - try { - Ensure-Directory -Path $script:IncidentArtifactsRoot - Ensure-ScreenshotTypesLoaded - - $vs = [System.Windows.Forms.SystemInformation]::VirtualScreen - $bmp = New-Object System.Drawing.Bitmap ([int]$vs.Width), ([int]$vs.Height) - $gfx = [System.Drawing.Graphics]::FromImage($bmp) - try { - $gfx.CopyFromScreen([int]$vs.Left, [int]$vs.Top, 0, 0, $bmp.Size) - $path = Get-IncidentScreenshotPath -RuleId $RuleId -SignalType $SignalType - $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) - } - finally { - $gfx.Dispose() - $bmp.Dispose() - } - - return @{ - screenshotPath = $path - screenshotFormat = 'png' - screenshotWidth = [int]$vs.Width - screenshotHeight = [int]$vs.Height - screenshotSha256 = (Get-FileSha256Hex -Path $path) - } - } - catch { - Write-EndpointLog ("screenshot capture failed: {0}" -f $_.Exception.Message) - return @{} - } -} - -# --------------------------------------------------------------------------- -# Enforcement functions (action = "block") -# --------------------------------------------------------------------------- - -function Show-EnforcementNotification { - param( - [Parameter(Mandatory = $true)][string]$Title, - [Parameter(Mandatory = $true)][string]$Body - ) - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue - $icon = New-Object System.Windows.Forms.NotifyIcon - $icon.Icon = [System.Drawing.SystemIcons]::Warning - $icon.BalloonTipTitle = $Title - $icon.BalloonTipText = $Body - $icon.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Warning - $icon.Visible = $true - $icon.ShowBalloonTip(5000) - Start-Sleep -Milliseconds 200 - $icon.Dispose() - } - catch { - Write-EndpointLog ("notification failed: {0}" -f $_.Exception.Message) - } -} - -function Invoke-ClipboardEnforcement { - [OutputType([bool])] - param() - try { - Set-Clipboard -Value $null -ErrorAction Stop - Write-EndpointLog "enforcement: clipboard cleared" - return $true - } - catch { - Write-EndpointLog ("enforcement: clipboard clear failed: {0}" -f $_.Exception.Message) - return $false - } -} - -function Invoke-UsbWriteBlockEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$DriveLetter - ) - try { - $partition = Get-Partition -DriveLetter ($DriveLetter.TrimEnd(':')) -ErrorAction Stop - $disk = Get-Disk -Number $partition.DiskNumber -ErrorAction Stop - if ($disk.BusType -ne 'USB') { - Write-EndpointLog ("enforcement: skip non-USB disk {0} bus={1}" -f $disk.Number, $disk.BusType) - return $false - } - if (-not $disk.IsReadOnly) { - Set-Disk -Number $disk.Number -IsReadOnly $true -ErrorAction Stop - Write-EndpointLog ("enforcement: USB disk {0} ({1}) set read-only" -f $disk.Number, $DriveLetter) - } - return $true - } - catch { - Write-EndpointLog ("enforcement: USB write-block failed drive={0}: {1}" -f $DriveLetter, $_.Exception.Message) - return $false - } -} - -function Invoke-PrintJobEnforcement { - [OutputType([bool])] - param( - [Parameter(Mandatory = $true)][string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - $cancelled = $false - try { - $jobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($jobs)) { - $jobPrinter = [string]$job.Name - $jobOwner = [string]$job.Owner - $jobDoc = [string]$job.Document - $matchPrinter = ($jobPrinter -like "*$PrinterName*") - $matchOwner = (-not $Owner) -or ($jobOwner -like "*$Owner*") -or ($jobOwner -like "*$env:USERNAME*") - if ($matchPrinter -and $matchOwner) { - Remove-CimInstance -InputObject $job -ErrorAction Stop - Write-EndpointLog ("enforcement: print job cancelled id={0} printer={1} doc={2}" -f $job.JobId, $jobPrinter, $jobDoc) - $cancelled = $true - } - } - } - catch { - Write-EndpointLog ("enforcement: print cancel failed printer={0}: {1}" -f $PrinterName, $_.Exception.Message) - } - return $cancelled -} - -function Get-StringHash { - param([AllowNull()][string]$Value) - if ($null -eq $Value) { return $null } - $bytes = [Text.Encoding]::UTF8.GetBytes($Value) - $sha = [Security.Cryptography.SHA256]::Create() - try { - ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') }) -join '' - } - finally { - $sha.Dispose() - } -} - -function Get-ClipboardTextSafe { - [OutputType([string])] - param() - - try { - $v = Get-Clipboard -Raw -ErrorAction Stop - if ($null -ne $v) { return [string]$v } - } - catch { - Write-EndpointLog ("clipboard direct read failed: {0}" -f $_.Exception.Message) - } - - # Fallback: read clipboard in a dedicated STA thread for RDP/user-session edge cases. - try { - Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue | Out-Null - $result = [string]::Empty - $thread = [System.Threading.Thread]{ - try { - $script:__aw_clip = [System.Windows.Forms.Clipboard]::GetText() - } - catch { - $script:__aw_clip = $null - } - } - $thread.SetApartmentState([System.Threading.ApartmentState]::STA) - $thread.Start() - $thread.Join(3000) | Out-Null - if ($thread.IsAlive) { $thread.Abort() } - $result = [string]$script:__aw_clip - Remove-Variable -Name __aw_clip -Scope Script -ErrorAction SilentlyContinue - return $result - } - catch { - Write-EndpointLog ("clipboard STA read failed: {0}" -f $_.Exception.Message) - return $null - } -} - -function Load-DlpPolicy { - param([string]$Path) - - $script:Policy = [ordered]@{ - defaults = [ordered]@{ - enabled = $true - cooldownSeconds = 300 - action = 'alert' - severity = 'medium' - } - endpoint = [ordered]@{ - clipboard = @() - usb = @() - print = @() - } - } - - if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { - Write-EndpointLog ("policy not found, using defaults: {0}" -f $Path) - return - } - - try { - $raw = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json - if ($raw.defaults) { - if ($raw.defaults.PSObject.Properties.Name -contains 'enabled') { $script:Policy.defaults.enabled = [bool]$raw.defaults.enabled } - if ($raw.defaults.cooldownSeconds) { $script:Policy.defaults.cooldownSeconds = [int]$raw.defaults.cooldownSeconds } - if ($raw.defaults.action) { $script:Policy.defaults.action = [string]$raw.defaults.action } - if ($raw.defaults.severity) { $script:Policy.defaults.severity = [string]$raw.defaults.severity } - } - - if ($raw.endpoint) { - if ($raw.endpoint.clipboard) { $script:Policy.endpoint.clipboard = @($raw.endpoint.clipboard) } - if ($raw.endpoint.usb) { $script:Policy.endpoint.usb = @($raw.endpoint.usb) } - if ($raw.endpoint.print) { $script:Policy.endpoint.print = @($raw.endpoint.print) } - } - } - catch { - Write-EndpointLog ("policy parse failed: {0}" -f $_.Exception.Message) - } -} - -function Should-EmitByCooldown { - param( - [string]$Fingerprint, - [int]$CooldownSeconds - ) - - $now = (Get-Date).ToUniversalTime() - if ($script:Cooldown.ContainsKey($Fingerprint)) { - $last = [datetime]$script:Cooldown[$Fingerprint] - if ((New-TimeSpan -Start $last -End $now).TotalSeconds -lt $CooldownSeconds) { - return $false - } - } - - $script:Cooldown[$Fingerprint] = $now - return $true -} - -function Evaluate-ClipboardRules { - param( - [string]$ClipboardText, - [string]$ClipboardHash - ) - - foreach ($rule in @($script:Policy.endpoint.clipboard)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - $minLength = if ($rule.minLength) { [int]$rule.minLength } else { 0 } - $regexPatterns = if ($rule.regexPatterns) { @($rule.regexPatterns) } else { @() } - if ($ClipboardText.Length -lt $minLength) { continue } - - $matched = $false - foreach ($pattern in $regexPatterns) { - if ($ClipboardText -match [string]$pattern) { - $matched = $true - break - } - } - - if (-not $matched) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "clipboard|$ruleId|$ClipboardHash|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Clipboard rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-ClipboardEnforcement - Show-EnforcementNotification -Title 'DLP: буфер обмена очищен' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'clipboard' -Data @{ - clipboardHash = $ClipboardHash - clipboardLength = $ClipboardText.Length - enforced = $enforced - } - Write-EndpointLog ("incident clipboard rule={0} action={1} severity={2} enforced={3}" -f $ruleId, $action, $severity, $enforced) - } -} - -function Evaluate-UsbRules { - param( - [string]$DriveLetter, - [string]$VolumeName - ) - - foreach ($rule in @($script:Policy.endpoint.usb)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "usb|$ruleId|$DriveLetter|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "USB rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-UsbWriteBlockEnforcement -DriveLetter $DriveLetter - Show-EnforcementNotification -Title 'DLP: USB заблокирован для записи' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'usb_insert' -Data @{ - driveLetter = $DriveLetter - volumeName = $VolumeName - enforced = $enforced - } - Write-EndpointLog ("incident usb rule={0} action={1} severity={2} drive={3} enforced={4}" -f $ruleId, $action, $severity, $DriveLetter, $enforced) - } -} - -function Evaluate-PrintRules { - param( - [string]$PrinterName, - [string]$DocumentName, - [string]$Owner - ) - - foreach ($rule in @($script:Policy.endpoint.print)) { - if (-not $rule) { continue } - if ($rule.PSObject.Properties.Name -contains 'enabled' -and -not [bool]$rule.enabled) { continue } - $ruleId = [string]$rule.id - if (-not $ruleId) { continue } - - $match = $true - if ($rule.printerRegex) { - $match = $match -and ($PrinterName -match [string]$rule.printerRegex) - } - if ($rule.documentRegex) { - $match = $match -and ($DocumentName -match [string]$rule.documentRegex) - } - if (-not $match) { continue } - - $cooldown = if ($rule.cooldownSeconds) { [int]$rule.cooldownSeconds } else { [int]$script:Policy.defaults.cooldownSeconds } - $fingerprint = "print|$ruleId|$PrinterName|$Owner|$env:USERNAME" - if (-not (Should-EmitByCooldown -Fingerprint $fingerprint -CooldownSeconds ([Math]::Max($cooldown, 30)))) { continue } - - $action = if ($rule.action) { [string]$rule.action } else { [string]$script:Policy.defaults.action } - $severity = if ($rule.severity) { [string]$rule.severity } else { [string]$script:Policy.defaults.severity } - $message = if ($rule.message) { [string]$rule.message } else { "Print rule matched: $ruleId" } - - $enforced = $false - if ($action -eq 'block') { - $enforced = Invoke-PrintJobEnforcement -PrinterName $PrinterName -DocumentName $DocumentName -Owner $Owner - Show-EnforcementNotification -Title 'DLP: печать заблокирована' -Body $message - } - - Send-DlpIncidentHeartbeat -RuleId $ruleId -Action $action -Severity $severity -Message $message -SignalType 'print_job' -Data @{ - printerName = $PrinterName - documentName = $DocumentName - owner = $Owner - enforced = $enforced - } - Write-EndpointLog ("incident print rule={0} action={1} severity={2} printer={3} enforced={4}" -f $ruleId, $action, $severity, $PrinterName, $enforced) - } -} - -function Test-LooksLikeMojibakeQuestionMarks { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return $true } - return $Value -match '\?{2,}' -} - -function Normalize-OwnerForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized -match '[\\/]') { - $parts = $normalized -split '[\\/]' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[$parts.Count - 1] - } - } - if ($normalized -match '@') { - $parts = $normalized -split '@' - if ($parts.Count -gt 0) { - $normalized = [string]$parts[0] - } - } - return $normalized -} - -function Test-OwnerLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-OwnerForMatch -Value $Expected - $actualNorm = Normalize-OwnerForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Normalize-PrinterForMatch { - param([AllowNull()][string]$Value) - if ([string]::IsNullOrWhiteSpace($Value)) { return '' } - $normalized = $Value.Trim().ToLowerInvariant() - if ($normalized.Contains(',')) { - $normalized = ($normalized -split ',', 2)[0].Trim() - } - if ($normalized -match '\son\s') { - $normalized = ($normalized -split '\son\s', 2)[0].Trim() - } - return $normalized -} - -function Test-PrinterLooseMatch { - param( - [string]$Expected, - [string]$Actual - ) - $expectedNorm = Normalize-PrinterForMatch -Value $Expected - $actualNorm = Normalize-PrinterForMatch -Value $Actual - if ([string]::IsNullOrWhiteSpace($expectedNorm) -or [string]::IsNullOrWhiteSpace($actualNorm)) { - return $false - } - return ($actualNorm -eq $expectedNorm) -or $actualNorm.Contains($expectedNorm) -or $expectedNorm.Contains($actualNorm) -} - -function Get-PrintServiceEventSummary { - param([Parameter(Mandatory = $true)]$Event) - - $props = @($Event.Properties) - $propertyValues = @() - foreach ($prop in $props) { - $propertyValues += [string]$prop.Value - } - - [pscustomobject]@{ - RecordId = [string]$Event.RecordId - TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' } - PropertyCount = $props.Count - DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' } - Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' } - PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' } - PropertyValues = $propertyValues - } -} - -function Get-PrintServiceDocumentFallback { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Owner, - [string]$PrinterName - ) - - $preferred = [string]$EventSummary.DocumentName - if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') { - return $preferred - } - - $pathCandidates = New-Object System.Collections.Generic.List[string] - $textCandidates = New-Object System.Collections.Generic.List[string] - - foreach ($value in @($EventSummary.PropertyValues)) { - $candidate = [string]$value - if ([string]::IsNullOrWhiteSpace($candidate)) { continue } - if ($candidate -eq $preferred) { continue } - if ($Owner -and $candidate -like "*$Owner*") { continue } - if ($PrinterName -and $candidate -like "*$PrinterName*") { continue } - if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue } - - if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') { - $pathCandidates.Add($candidate) - continue - } - - if ($candidate -match '^[0-9]+$') { - continue - } - - $textCandidates.Add($candidate) - } - - foreach ($candidate in @($pathCandidates)) { - $leaf = Split-Path -Path $candidate -Leaf - if (-not [string]::IsNullOrWhiteSpace($leaf)) { - return $leaf - } - return $candidate - } - - foreach ($candidate in @($textCandidates)) { - return $candidate - } - - return $null -} - -function Write-PrintServiceEventTrace { - param( - [Parameter(Mandatory = $true)]$EventSummary, - [string]$Phase, - [string]$MatchReason, - [string]$ResolvedDocument - ) - - $properties = if ($EventSummary.PropertyValues) { - ($EventSummary.PropertyValues -join ' | ') - } - else { - '' - } - - Write-EndpointLog ( - 'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f - $Phase, - $EventSummary.RecordId, - $EventSummary.TimeCreated, - $EventSummary.Owner, - $EventSummary.PrinterName, - $EventSummary.DocumentName, - $ResolvedDocument, - $properties, - $MatchReason - ) -} - -function Get-BetterDocumentNameFromPrintServiceEvents { - param( - [string]$Owner, - [string]$PrinterName - ) - - try { - $startTime = (Get-Date).AddMinutes(-15) - $events = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = $startTime - } -MaxEvents 200 -ErrorAction Stop - - foreach ($pass in @('strict', 'relaxed')) { - foreach ($event in @($events)) { - $summary = Get-PrintServiceEventSummary -Event $event - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName - - $ownerMatches = if ($Owner) { Test-OwnerLooseMatch -Expected $Owner -Actual $summary.Owner } else { $true } - $printerMatches = if ($PrinterName) { Test-PrinterLooseMatch -Expected $PrinterName -Actual $summary.PrinterName } else { $true } - - if ($pass -eq 'strict') { - if ($Owner -and -not $ownerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - if ($PrinterName -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'printer-mismatch-strict' -ResolvedDocument $resolvedDocument - continue - } - } - else { - if ($Owner -and $PrinterName -and -not $ownerMatches -and -not $printerMatches) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason 'owner-and-printer-mismatch-relaxed' -ResolvedDocument $resolvedDocument - continue - } - } - - if ([string]::IsNullOrWhiteSpace($resolvedDocument)) { - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason ('no-document-candidate-' + $pass) -ResolvedDocument '' - continue - } - - $matchReasonBase = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' } - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason ($matchReasonBase + '-' + $pass) -ResolvedDocument $resolvedDocument - return $resolvedDocument - } - } - } - catch { - } - - return $null -} - -$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath -$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' } -$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 } -$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' } -$resolvedPolicyPath = if ($PolicyPath) { $PolicyPath } elseif ($deploymentConfig -and $deploymentConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$deploymentConfig.paths.policyPath } else { 'C:\ProgramData\AWatch-rus\dlp-policy.json' } -$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 } -$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' } -$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("endpoint-signals-{0}.log" -f $env:USERNAME) } -$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true } -$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' } -$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true } - -if ($resolvedLocalAgentLogsEnabled -and -not (Test-Path -LiteralPath $resolvedLogsRoot)) { - New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null -} - -$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort -$script:Hostname = $env:COMPUTERNAME -$script:SessionId = (Get-Process -Id $PID).SessionId -$script:KnownBuckets = @{} -$script:Cooldown = @{} -$script:SeenUsb = @{} -$script:SeenPrintJob = @{} -$script:SeenPrintEvent = @{} -$script:LastClipboardHash = $null -$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) -$script:SelfTestIntervalSeconds = [Math]::Max($resolvedPollSeconds * 10, 60) -$script:LastSelfTestAt = [datetime]::MinValue -$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled -$script:LogPath = $resolvedLogPath -$script:IncidentArtifactsRoot = $resolvedIncidentArtifactsRoot -$script:IncidentScreenshotEnabled = $resolvedIncidentScreenshotEnabled -$script:ScreenshotTypesLoaded = $false - -Load-DlpPolicy -Path $resolvedPolicyPath -Write-EndpointLog ("endpoint collector started against {0}" -f $script:ApiBase) - -while ($true) { - try { - $nowUtc = (Get-Date).ToUniversalTime() - if (($nowUtc - $script:LastSelfTestAt).TotalSeconds -ge $script:SelfTestIntervalSeconds) { - Send-EndpointSignalHeartbeat -SignalType 'self_test' -Data @{ - collector = 'dlp-endpoint-signals' - policyEnabled = [bool]$script:Policy.defaults.enabled - } - $script:LastSelfTestAt = $nowUtc - } - - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } - - try { - $clipboardText = Get-ClipboardTextSafe - if ($clipboardText) { - $clipboardHash = Get-StringHash -Value $clipboardText - if ($clipboardHash -and $clipboardHash -ne $script:LastClipboardHash) { - $script:LastClipboardHash = $clipboardHash - Send-EndpointSignalHeartbeat -SignalType 'clipboard_change' -Data @{ - clipboardHash = $clipboardHash - clipboardLength = $clipboardText.Length - } - Evaluate-ClipboardRules -ClipboardText $clipboardText -ClipboardHash $clipboardHash - } - } - } - catch { - } - - try { - $usbDrives = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=2" -ErrorAction SilentlyContinue - $currentUsb = @{} - foreach ($drive in @($usbDrives)) { - $deviceId = [string]$drive.DeviceID - if (-not $deviceId) { continue } - $currentUsb[$deviceId] = $true - if (-not $script:SeenUsb.ContainsKey($deviceId)) { - $script:SeenUsb[$deviceId] = (Get-Date).ToUniversalTime() - $volumeName = [string]$drive.VolumeName - Send-EndpointSignalHeartbeat -SignalType 'usb_insert' -Data @{ - driveLetter = $deviceId - volumeName = $volumeName - } - Evaluate-UsbRules -DriveLetter $deviceId -VolumeName $volumeName - } - } - - foreach ($known in @($script:SeenUsb.Keys)) { - if (-not $currentUsb.ContainsKey($known)) { - $script:SeenUsb.Remove($known) - } - } - } - catch { - } - - try { - $printJobs = Get-CimInstance Win32_PrintJob -ErrorAction SilentlyContinue - foreach ($job in @($printJobs)) { - $jobId = [string]$job.JobId - if (-not $jobId) { continue } - if ($script:SeenPrintJob.ContainsKey($jobId)) { continue } - $script:SeenPrintJob[$jobId] = (Get-Date).ToUniversalTime() - - $printerName = [string]$job.Name - $documentName = [string]$job.Document - $owner = [string]$job.Owner - $documentNameOriginal = $documentName - - if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) { - $eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName - if ($eventDocumentName) { - $documentName = $eventDocumentName - } - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = $documentName - documentNameOriginal = $documentNameOriginal - owner = $owner - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner - } - - $cleanupBefore = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintJob.Keys)) { - $ts = [datetime]$script:SeenPrintJob[$k] - if ($ts -lt $cleanupBefore) { - $script:SeenPrintJob.Remove($k) - } - } - } - catch { - } - - try { - $printEvents = Get-WinEvent -FilterHashtable @{ - LogName = 'Microsoft-Windows-PrintService/Operational' - Id = 307 - StartTime = (Get-Date).AddMinutes(-20) - } -MaxEvents 200 -ErrorAction SilentlyContinue - - foreach ($event in @($printEvents)) { - $recordId = [string]$event.RecordId - if (-not $recordId) { continue } - if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue } - $script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime() - - $summary = Get-PrintServiceEventSummary -Event $event - $documentName = [string]$summary.DocumentName - $owner = [string]$summary.Owner - $printerName = [string]$summary.PrinterName - $resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName - - Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument - - if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") { - continue - } - - Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{ - printerName = $printerName - documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName } - documentNameOriginal = $documentName - owner = $owner - eventRecordId = $recordId - eventSource = 'printservice-307' - } - Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner - } - - $cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8) - foreach ($k in @($script:SeenPrintEvent.Keys)) { - $ts = [datetime]$script:SeenPrintEvent[$k] - if ($ts -lt $cleanupBeforeEvent) { - $script:SeenPrintEvent.Remove($k) - } - } - } - catch { - } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} -; } - } - catch { - Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message) - } - - Start-Sleep -Seconds $resolvedPollSeconds -} diff --git a/windows/email-outbound-collector.ps1 b/windows/email-outbound-collector.ps1 index d7f4d94..7a1600f 100644 --- a/windows/email-outbound-collector.ps1 +++ b/windows/email-outbound-collector.ps1 @@ -53,79 +53,13 @@ function Write-CollectorLog { catch { } } -function Add-WalEntry { +function Invoke-AwJsonPost { param( [Parameter(Mandatory = $true)][string]$Uri, [Parameter(Mandatory = $true)][string]$Json ) - if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return } - try { - $entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress - Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8 - } catch {} -} - -function Flush-Wal { - if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return } - $remaining = New-Object System.Collections.Generic.List[string] - try { - $script:WalFlushing = $true - foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - try { - $entry = $line | ConvertFrom-Json - if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue } - if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) } - } catch { $remaining.Add($line) } - } - if ($remaining.Count -eq 0) { - Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue - } else { - Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8 - } - } finally { - $script:WalFlushing = $false - } -} - -function Write-CollectorHealth { - param([string]$Status = 'running') - if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return } - try { - $walDepth = 0 - if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count } - $health = @{ - collector = 'email-outbound'; hostname = $script:Hostname; sessionId = $script:SessionId; - status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Depth 5 - Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8 - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json, - [int]$MaxAttempts = 5, - [int]$InitialBackoffMs = 500 - ) - $attempt = 1 - $backoff = [Math]::Max(100, $InitialBackoffMs) $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - while ($attempt -le $MaxAttempts) { - try { - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null - return $true - } catch { - if ($attempt -ge $MaxAttempts) { - if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json } - return $false - } - Start-Sleep -Milliseconds $backoff - $backoff = [Math]::Min($backoff * 2, 10000) - $attempt++ - } - } + Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null } function Ensure-Bucket { @@ -583,10 +517,6 @@ $script:SeenSmtpConnections = @{} $script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30) $script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled $script:LogPath = $resolvedLogPath -$stateRoot = if ($deploymentConfig -and $deploymentConfig.paths -and $deploymentConfig.paths.stateRoot) { [string]$deploymentConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' } -$script:WalPath = Join-Path $stateRoot 'wal-email-outbound.ndjson' -$script:HealthPath = Join-Path $stateRoot 'health-email-outbound.json' -$script:WalFlushing = $false $script:OutlookApp = $null $script:OutlookNamespace = $null $script:SentFolder = $null @@ -610,65 +540,43 @@ if ($useOutlook) { # Main loop # --------------------------------------------------------------------------- -try { - while ($true) { - try { - Flush-Wal - Write-CollectorHealth -Status 'running' - if (-not $script:Policy.defaults.enabled) { - Start-Sleep -Seconds $resolvedPollSeconds - continue - } +while ($true) { + try { + if (-not $script:Policy.defaults.enabled) { + Start-Sleep -Seconds $resolvedPollSeconds + continue + } - if ($useOutlook) { - if (-not $outlookReady) { - $outlookReady = Initialize-OutlookCom - } - if ($outlookReady) { - try { - Poll-OutlookSentItems - } - catch { - Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) - $outlookReady = $false - $script:OutlookApp = $null - $script:OutlookNamespace = $null - $script:SentFolder = $null - } - } + if ($useOutlook) { + if (-not $outlookReady) { + $outlookReady = Initialize-OutlookCom } - - if ($useSmtp) { + if ($outlookReady) { try { - Poll-SmtpConnections + Poll-OutlookSentItems } catch { - Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) + Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message) + $outlookReady = $false + $script:OutlookApp = $null + $script:OutlookNamespace = $null + $script:SentFolder = $null } } } - catch { - Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) - } - Start-Sleep -Seconds $resolvedPollSeconds - } -} -finally { - Write-CollectorHealth -Status 'stopped' - try { - if ($null -ne $script:SentFolder) { - [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:SentFolder) - $script:SentFolder = $null - } - if ($null -ne $script:OutlookNamespace) { - [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookNamespace) - $script:OutlookNamespace = $null - } - if ($null -ne $script:OutlookApp) { - [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookApp) - $script:OutlookApp = $null + if ($useSmtp) { + try { + Poll-SmtpConnections + } + catch { + Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message) + } } } - catch {} + catch { + Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message) + } + + Start-Sleep -Seconds $resolvedPollSeconds } diff --git a/windows/file-operations-collector.ps1 b/windows/file-operations-collector.ps1 index e0890e9..bc63d55 100644 --- a/windows/file-operations-collector.ps1 +++ b/windows/file-operations-collector.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -22,9 +22,6 @@ Add-Type -AssemblyName System.Net.Http $script:KnownBuckets = @{} $script:Hostname = $env:COMPUTERNAME $script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId -$script:WalPath = $null -$script:HealthPath = $null -$script:WalFlushing = $false # Настройка логирования $script:LogPath = $LogPath @@ -46,88 +43,28 @@ function Write-FileCollectorLog { } catch {} } -function Add-WalEntry { +function Invoke-AwJsonPost { param( [Parameter(Mandatory = $true)][string]$Uri, [Parameter(Mandatory = $true)][string]$Json ) - if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return } + $httpClient = $null try { - $entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress - Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8 - } catch {} -} - -function Flush-Wal { - if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return } - $remaining = New-Object System.Collections.Generic.List[string] - try { - $script:WalFlushing = $true - foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) { - if ([string]::IsNullOrWhiteSpace($line)) { continue } - try { - $entry = $line | ConvertFrom-Json - if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue } - if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) } - } catch { $remaining.Add($line) } - } - if ($remaining.Count -eq 0) { - Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue - } else { - Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8 + $httpClient = New-Object System.Net.Http.HttpClient + $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") + $response = $httpClient.PostAsync($Uri, $content).Result + if (-not $response.IsSuccessStatusCode) { + $status = [int]$response.StatusCode + $reason = [string]$response.ReasonPhrase + $body = $response.Content.ReadAsStringAsync().Result + Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body) } + } catch { + Write-FileCollectorLog "POST Error: $($_.Exception.Message)" } finally { - $script:WalFlushing = $false - } -} - -function Write-CollectorHealth { - param([string]$Status = 'running') - if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return } - try { - $walDepth = 0 - if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count } - $health = @{ - collector = 'file-operations'; hostname = $script:Hostname; sessionId = $script:SessionId; - status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o') - } | ConvertTo-Json -Depth 5 - Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8 - } catch {} -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json, - [int]$MaxAttempts = 5, - [int]$InitialBackoffMs = 500 - ) - $attempt = 1 - $backoff = [Math]::Max(100, $InitialBackoffMs) - while ($attempt -le $MaxAttempts) { - $httpClient = $null - try { - $httpClient = New-Object System.Net.Http.HttpClient - $content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json") - $response = $httpClient.PostAsync($Uri, $content).Result - if ($response.IsSuccessStatusCode) { return $true } - if ($attempt -ge $MaxAttempts) { - if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json } - return $false - } - } catch { - if ($attempt -ge $MaxAttempts) { - if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json } - return $false - } - } finally { - if ($null -ne $httpClient) { - $httpClient.Dispose() - } + if ($null -ne $httpClient) { + $httpClient.Dispose() } - Start-Sleep -Milliseconds $backoff - $backoff = [Math]::Min($backoff * 2, 10000) - $attempt++ } } @@ -208,15 +145,11 @@ function Send-FileOperationEvent { $config = Get-DeploymentConfig -Path $ConfigPath if (-not $config) { throw "Configuration file not found: $ConfigPath" } -$script:Hostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME } $scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) { $config.server.scheme } else { 'http' } $hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' } $port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 } $script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port -$stateRoot = if ($config.paths -and $config.paths.stateRoot) { [string]$config.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' } -$script:WalPath = Join-Path $stateRoot 'wal-file-operations.ndjson' -$script:HealthPath = Join-Path $stateRoot 'health-file-operations.json' $bucketId = 'aw-file-operations_' + $script:Hostname Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation' @@ -273,14 +206,11 @@ Write-FileCollectorLog "Collector started. Waiting for events..." try { while ($true) { - Flush-Wal - Write-CollectorHealth -Status 'running' Start-Sleep -Seconds $PollSeconds } } finally { Write-FileCollectorLog "Stopping collector..." - Write-CollectorHealth -Status 'stopped' foreach ($sub in @($subscriptions)) { try { if ($sub -and $sub.Id) { diff --git a/windows/hardening-recovery.ps1 b/windows/hardening-recovery.ps1 index ed4bbac..d382f41 100755 --- a/windows/hardening-recovery.ps1 +++ b/windows/hardening-recovery.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding()] +[CmdletBinding()] param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$ServerHost, @@ -21,7 +21,6 @@ param( [bool]$IncidentScreenshotEnabled, [string]$IncidentArtifactsRoot, [bool]$LogonMarkerEnabled, - [string]$AwHostname, [string]$CustomRulesPath, [string]$CustomPolicyPath, [switch]$RepairPackage, @@ -74,7 +73,6 @@ $effectiveIncidentCaptureEnabled = if ($PSBoundParameters.ContainsKey('IncidentC $effectiveIncidentScreenshotEnabled = if ($PSBoundParameters.ContainsKey('IncidentScreenshotEnabled')) { [bool]$IncidentScreenshotEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$existingConfig.incidentCapture.screenshotEnabled } else { $true } $effectiveIncidentArtifactsRoot = if ($PSBoundParameters.ContainsKey('IncidentArtifactsRoot') -and $IncidentArtifactsRoot) { $IncidentArtifactsRoot } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $existingConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$existingConfig.incidentCapture.artifactsRoot } else { Join-Path $effectiveStateRoot 'incident-artifacts' } $effectiveLogonMarkerEnabled = if ($PSBoundParameters.ContainsKey('LogonMarkerEnabled')) { [bool]$LogonMarkerEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'sessionEvents' -and $existingConfig.sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$existingConfig.sessionEvents.logonEnabled } else { $true } -$effectiveAwHostname = if ($PSBoundParameters.ContainsKey('AwHostname') -and -not [string]::IsNullOrWhiteSpace($AwHostname)) { [string]$AwHostname } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$existingConfig.awHostname)) { [string]$existingConfig.awHostname } else { [string]$env:COMPUTERNAME } $effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' } $effectiveUsers = if ($Users -or $UserListPath) { @@ -141,7 +139,6 @@ $config = New-ActivityWatchDeploymentConfig ` -IncidentScreenshotEnabled $effectiveIncidentScreenshotEnabled ` -IncidentArtifactsRoot $effectiveIncidentArtifactsRoot ` -LogonMarkerEnabled $effectiveLogonMarkerEnabled ` - -AwHostname $effectiveAwHostname ` -LaunchScriptPath $effectiveLaunchScript ` -RecoveryScriptPath $effectiveRecoveryScript ` -UserTasks $taskDefinitions ` @@ -154,6 +151,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName -Write-Output 'Укрепление и восстановление ActivityWatch завершены.' -Write-Output "Конфигурация: $effectiveConfigPath" -Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')" +Write-Host 'Укрепление и восстановление ActivityWatch завершены.' +Write-Host "Конфигурация: $effectiveConfigPath" +Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')" diff --git a/windows/install-standalone-service.ps1 b/windows/install-standalone-service.ps1 deleted file mode 100644 index e7ef129..0000000 --- a/windows/install-standalone-service.ps1 +++ /dev/null @@ -1,121 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [string]$ServerHost, - [int]$ServerPort = 5600, - [ValidateSet('http', 'https')] - [string]$ServerScheme = 'http', - [string]$StateRoot = 'C:\ProgramData\AWatch-rus', - [string]$InstallRoot = 'C:\Program Files\AWatch-rus\bin', - [string]$ServiceName = 'AWatchRusStandaloneAgent', - [string]$AwHostname -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Assert-Admin { - $id = [Security.Principal.WindowsIdentity]::GetCurrent() - $p = [Security.Principal.WindowsPrincipal]::new($id) - if (-not $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw 'Run as Administrator.' - } -} - -function Ensure-Dir { - param([string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { - New-Item -Path $Path -ItemType Directory -Force | Out-Null - } -} - -Assert-Admin - -$logsRoot = Join-Path $StateRoot 'logs' -Ensure-Dir -Path $StateRoot -Ensure-Dir -Path $logsRoot - -$collectorScript = Join-Path $StateRoot 'browser-domains-native-collector.ps1' -$endpointCollectorScript = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1' -$fileCollectorScript = Join-Path $StateRoot 'file-operations-collector.ps1' -$emailCollectorScript = Join-Path $StateRoot 'email-outbound-collector.ps1' -$sessionCollectorScript = Join-Path $StateRoot 'worktime-session-collector.ps1' -$rulesPath = Join-Path $StateRoot 'web-category-rules.json' -$policyPath = Join-Path $StateRoot 'dlp-policy.json' -$configPath = Join-Path $StateRoot 'deployment-config.json' -$serviceScriptPath = Join-Path $PSScriptRoot 'aw-standalone-service.ps1' - -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') -Destination $collectorScript -Force -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') -Destination $endpointCollectorScript -Force -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'file-operations-collector.ps1') -Destination $fileCollectorScript -Force -if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1')) { - Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'email-outbound-collector.ps1') -Destination $emailCollectorScript -Force -} -if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1')) { - Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'worktime-session-collector.ps1') -Destination $sessionCollectorScript -Force -} - -if (-not (Test-Path -LiteralPath $rulesPath)) { - Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'web-category-rules.example.json') -Destination $rulesPath -Force -} -if (-not (Test-Path -LiteralPath $policyPath)) { - Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'dlp-policy.example.json') -Destination $policyPath -Force -} - -$effectiveHostname = if ([string]::IsNullOrWhiteSpace($AwHostname)) { [string]$env:COMPUTERNAME } else { [string]$AwHostname } - -$config = [pscustomobject]@{ - version = 1 - generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') - awHostname = $effectiveHostname - server = [pscustomobject]@{ - host = $ServerHost - port = $ServerPort - scheme = $ServerScheme - } - paths = [pscustomobject]@{ - installRoot = $InstallRoot - stateRoot = $StateRoot - logsRoot = $logsRoot - collectorScript = $collectorScript - endpointCollectorScript = $endpointCollectorScript - fileCollectorScript = $fileCollectorScript - emailCollectorScript = $emailCollectorScript - sessionCollectorScript = $sessionCollectorScript - rulesPath = $rulesPath - policyPath = $policyPath - } - collector = [pscustomobject]@{ - pollSeconds = 5 - pulseSeconds = 30 - } - collectors = [pscustomobject]@{ - afkEnabled = $false - windowEnabled = $false - fileOpsEnabled = $true - emailEnabled = $true - } - logging = [pscustomobject]@{ - localAgentLogsEnabled = $true - } -} - -$config | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $configPath -Encoding UTF8 - -$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue -if ($existing) { - sc.exe stop $ServiceName | Out-Null - Start-Sleep -Seconds 1 - sc.exe delete $ServiceName | Out-Null - Start-Sleep -Seconds 1 -} - -$binPath = "`"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`" -NoProfile -ExecutionPolicy Bypass -File `"$serviceScriptPath`" -ConfigPath `"$configPath`"" -sc.exe create $ServiceName binPath= "$binPath" start= auto DisplayName= "AWatch-rus Standalone Agent" | Out-Null -sc.exe description $ServiceName "Standalone AWatch-rus DLP agent service wrapper" | Out-Null -sc.exe failure $ServiceName reset= 60 actions= restart/5000/restart/5000/restart/5000 | Out-Null -sc.exe start $ServiceName | Out-Null - -Write-Output "Standalone service installed: $ServiceName" -Write-Output "Config: $configPath" -Write-Output ("Host: {0} -> {1}://{2}:{3}" -f $effectiveHostname, $ServerScheme, $ServerHost, $ServerPort) diff --git a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss index d79a51c..cab5adc 100644 --- a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss +++ b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss @@ -34,8 +34,6 @@ Name: "validate"; Description: "Запустить validate-deployment (чере [Files] Source: "..\..\ActivityWatch.Windows.Common.psd1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\ActivityWatch.Windows.Common.psm1"; DestDir: "{app}\windows"; Flags: ignoreversion -Source: "..\..\install-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion -Source: "..\..\aw-standalone-service.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-single-user.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion @@ -45,7 +43,6 @@ Source: "..\..\migrate-awatch-rus-paths.ps1"; DestDir: "{app}\windows"; Flags: i Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion -Source: "..\..\file-operations-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\email-outbound-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\web-category-rules.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\dlp-policy.example.json"; DestDir: "{app}\windows"; Flags: ignoreversion @@ -54,11 +51,95 @@ Source: "payload\{#AwDefaultZipName}"; DestDir: "{app}\payload"; Flags: ignoreve Source: "innosetup-rdp-package-filelist.md"; DestDir: "{app}\windows\installkit\innosetup"; Flags: ignoreversion [Run] -Filename: "powershell.exe"; Parameters: "{code:GetStandaloneInstallParams}"; Flags: runhidden; Tasks: deploy +Filename: "powershell.exe"; Parameters: "{code:GetDeployEnsembleParams}"; Flags: runhidden; Tasks: deploy [Code] var ServerHostPage: TInputQueryWizardPage; + UsersPage: TInputQueryWizardPage; + OptionsPage: TInputOptionWizardPage; + +function NormalizeUserCsv(const UserCsv: string): string; +var + i: Integer; + s: string; + token: string; +begin + Result := ''; + s := UserCsv; + while True do + begin + i := Pos(',', s); + if i = 0 then + begin + token := Trim(s); + s := ''; + end + else + begin + token := Trim(Copy(s, 1, i - 1)); + Delete(s, 1, i); + end; + + if token <> '' then + begin + if Result <> '' then + Result := Result + ','; + Result := Result + token; + end; + + if s = '' then + Break; + end; +end; + +function BuildUsersPowerShellArg(const UserCsv: string): string; +var + i: Integer; + s: string; + token: string; + quoted: string; +begin + Result := ''; + s := UserCsv; + while True do + begin + i := Pos(',', s); + if i = 0 then + begin + token := Trim(s); + s := ''; + end + else + begin + token := Trim(Copy(s, 1, i - 1)); + Delete(s, 1, i); + end; + + if token <> '' then + begin + quoted := '"' + token + '"'; + if Result <> '' then + Result := Result + ','; + Result := Result + quoted; + end; + + if s = '' then + Break; + end; + if Result <> '' then + Result := '-Users ' + Result; +end; + +function PayloadZipPath: string; +begin + Result := ExpandConstant('{app}\payload\{#AwDefaultZipName}'); +end; + +function HasPayloadZip: Boolean; +begin + Result := FileExists(ExpandConstant('{src}\payload\{#AwDefaultZipName}')); +end; procedure InitializeWizard; begin @@ -74,24 +155,62 @@ begin ServerHostPage.Add('ServerPort', False); ServerHostPage.Values[0] := '{#AwDefaultServerHost}'; ServerHostPage.Values[1] := '{#AwDefaultServerPort}'; + + UsersPage := CreateInputQueryPage( + ServerHostPage.ID, + 'Пользователи (RDP)', + 'Перечень пользователей, для которых разворачиваем агенты.', + 'Введите список через запятую. Пример: user1,user2,user3' + ); + UsersPage.Add('Users (CSV)', False); + UsersPage.Values[0] := '{#AwDefaultUsers}'; + + OptionsPage := CreateInputOptionPage( + UsersPage.ID, + 'Опции деплоя', + 'Выберите опции для установки/валидации.', + '', + False, + False + ); + OptionsPage.Add('Использовать offline payload (встроенный ZIP)'); + OptionsPage.Add('Запустить validate-deployment после деплоя'); + OptionsPage.Values[0] := HasPayloadZip; + OptionsPage.Values[1] := True; end; -function GetStandaloneInstallParams(Param: string): string; +function GetDeployEnsembleParams(Param: string): string; var serverHost: string; serverPort: string; + usersCsv: string; + usersArg: string; + zipArg: string; + validateArg: string; begin serverHost := Trim(ServerHostPage.Values[0]); serverPort := Trim(ServerHostPage.Values[1]); - if serverHost = '' then - RaiseException('ServerHost is empty.'); - if serverPort = '' then - RaiseException('ServerPort is empty.'); + usersCsv := NormalizeUserCsv(UsersPage.Values[0]); + + usersArg := BuildUsersPowerShellArg(usersCsv); + if usersArg = '' then + RaiseException('Users list is empty.'); + + zipArg := ''; + if OptionsPage.Values[0] then + zipArg := ' -PackageZipPath "' + PayloadZipPath + '"'; + + validateArg := ''; + if OptionsPage.Values[1] and WizardIsTaskSelected('validate') then + validateArg := ' -ValidateAfterDeploy'; Result := - '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\install-standalone-service.ps1') + '"' + + '-NoProfile -ExecutionPolicy Bypass -File "' + ExpandConstant('{app}\windows\deploy-ensemble.ps1') + '"' + ' -ServerHost "' + serverHost + '"' + ' -ServerPort ' + serverPort + + ' ' + usersArg + + zipArg + ' -InstallRoot "{#AwDefaultInstallRoot}"' + - ' -StateRoot "{#AwDefaultStateRoot}"'; + ' -StateRoot "{#AwDefaultStateRoot}"' + + validateArg; end; diff --git a/windows/migrate-awatch-rus-paths.ps1 b/windows/migrate-awatch-rus-paths.ps1 index f91e6eb..c588050 100644 --- a/windows/migrate-awatch-rus-paths.ps1 +++ b/windows/migrate-awatch-rus-paths.ps1 @@ -1,4 +1,4 @@ -[CmdletBinding(SupportsShouldProcess = $true)] +[CmdletBinding(SupportsShouldProcess = $true)] param( [string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2', [string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2', diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 index 15a4822..41b6bd9 100644 --- a/windows/validate-deployment.ps1 +++ b/windows/validate-deployment.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json' + [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json' ) Set-StrictMode -Version Latest @@ -13,26 +13,70 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath $installRoot = [string]$config.paths.installRoot $stateRoot = [string]$config.paths.stateRoot $collectorScript = [string]$config.paths.collectorScript +$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' } +$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' } +$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' } $rulesPath = [string]$config.paths.rulesPath +$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' } $launchScript = [string]$config.paths.launchScript $recoveryScript = [string]$config.paths.recoveryScript +$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true } +$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true } +$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true } +$printServiceOperationalEnabled = $false +try { + $printServiceLog = Get-WinEvent -ListLog 'Microsoft-Windows-PrintService/Operational' -ErrorAction Stop + $printServiceOperationalEnabled = [bool]$printServiceLog.IsEnabled +} +catch { +} +$printJobTitlePolicyEnabled = $false +try { + $printPolicy = Get-ItemProperty -LiteralPath 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' -Name 'ShowJobTitleInEventLogs' -ErrorAction Stop + $printJobTitlePolicyEnabled = ([int]$printPolicy.ShowJobTitleInEventLogs -eq 1) +} +catch { +} $requiredFiles = @( - (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'), - (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), $collectorScript, + $endpointCollectorScript, + $sessionCollectorScript, $rulesPath, + $policyPath, $launchScript, $recoveryScript, $ConfigPath ) +if ($fileOpsExpected) { + $requiredFiles += $fileCollectorScript +} +if ($afkExpected) { + $requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe') +} +if ($windowExpected) { + $requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe') +} $missingFiles = @( $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) } ) -$processNames = @('aw-watcher-afk', 'aw-watcher-window') -$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId +$processNames = @() +if ($afkExpected) { $processNames += 'aw-watcher-afk' } +if ($windowExpected) { $processNames += 'aw-watcher-window' } +$runningProcesses = @() +if ($processNames.Count -gt 0) { + $runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId +} +$sessionCollectorProcesses = @( + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | + Where-Object { + ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and + $_.CommandLine -match [Regex]::Escape($sessionCollectorScript) + } | + Select-Object Name, ProcessId, SessionId, CommandLine +) $taskNames = @() if ($config.userTasks) { @@ -41,25 +85,28 @@ if ($config.userTasks) { $taskNames += [string]$config.recovery.taskName $taskNames = $taskNames | Sort-Object -Unique -$tasks = foreach ($taskName in $taskNames) { - $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue - if ($task) { - [pscustomobject]@{ - taskName = $task.TaskName - state = [string]$task.State - present = $true +$tasks = @( + foreach ($taskName in $taskNames) { + $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1 + if ($task) { + [pscustomobject]@{ + taskName = $task.TaskName + state = [string]$task.State + present = $true + } + } + else { + [pscustomobject]@{ + taskName = $taskName + state = 'Отсутствует' + present = $false + } } } - else { - [pscustomobject]@{ - taskName = $taskName - state = 'Missing' - present = $false - } - } -} +) $serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port +$uniqueRunningProcessNames = @($runningProcesses | Select-Object -ExpandProperty Name -Unique) $result = [ordered]@{ generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') configPath = $ConfigPath @@ -76,11 +123,24 @@ $result = [ordered]@{ ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present })) } processes = [ordered]@{ + expected = $processNames list = @($runningProcesses) - ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) + sessionCollectors = @($sessionCollectorProcesses) + ok = [bool]( + ( + ($processNames.Count -eq 0) -or + ($uniqueRunningProcessNames.Count -ge $processNames.Count) + ) -and + ($sessionCollectorProcesses.Count -ge 1) + ) + } + printTelemetry = [ordered]@{ + operationalLogEnabled = $printServiceOperationalEnabled + jobTitlePolicyEnabled = $printJobTitlePolicyEnabled + ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled) } } -$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok) +$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok) $result diff --git a/windows/worktime-session-collector.ps1 b/windows/worktime-session-collector.ps1 index df0b51d..7ebf555 100644 --- a/windows/worktime-session-collector.ps1 +++ b/windows/worktime-session-collector.ps1 @@ -1,268 +1,4 @@ -param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { Write-Error param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { - } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { - } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } elseif ($cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { - } - } - - Start-Sleep -Seconds $sleepSec -} -; } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { Write-Error param( +param( [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', [string]$Hostname, [int]$PollSeconds = 30 @@ -428,234 +164,3 @@ while ($true) { Start-Sleep -Seconds $sleepSec } -; } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { Write-Error param( - [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json', - [string]$Hostname, - [int]$PollSeconds = 30 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -function Get-Config { - param([string]$Path) - - if (-not (Test-Path -LiteralPath $Path)) { - throw "Конфигурация не найдена: $Path" - } - - Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json -} - -function Invoke-AwJsonPost { - param( - [Parameter(Mandatory = $true)][string]$Uri, - [Parameter(Mandatory = $true)][string]$Json - ) - - $bytes = [Text.Encoding]::UTF8.GetBytes($Json) - Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null -} - -function Ensure-Bucket { - param( - [Parameter(Mandatory = $true)][string]$ApiBase, - [Parameter(Mandatory = $true)][string]$BucketId, - [Parameter(Mandatory = $true)][string]$HostnameValue - ) - - try { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - return - } - catch { - } - - $body = @{ - client = 'aw-worktime-session-collector' - type = 'aw.worktime.session' - hostname = $HostnameValue - } | ConvertTo-Json -Compress - - try { - Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body - } - catch { - Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null - } -} - -function Get-SessionRecords { - $records = @() - - try { - $lines = quser 2>$null - if (-not $lines) { - return @() - } - - foreach ($line in ($lines | Select-Object -Skip 1)) { - $clean = ($line -replace '^\s*>?', '').Trim() - if (-not $clean) { - continue - } - - $parts = $clean -split '\s+' - if ($parts.Count -lt 4) { - continue - } - - $sessionName = '' - $sessionIdIndex = 2 - if ($parts[1] -match '^\d+$') { - $sessionIdIndex = 1 - } - else { - $sessionName = $parts[1] - } - - $sessionId = 0 - if ($parts[$sessionIdIndex] -match '^\d+$') { - $sessionId = [int]$parts[$sessionIdIndex] - } - - $records += [pscustomobject]@{ - username = $parts[0] - sessionName = $sessionName - sessionId = $sessionId - state = $parts[$sessionIdIndex + 1] - } - } - } - catch { - } - - return $records -} - -function Test-SessionIsActive { - param([AllowNull()][string]$State) - if ([string]::IsNullOrWhiteSpace($State)) { return $false } - $s = $State.Trim().ToLowerInvariant() - return ($s -eq 'active') -or ($s -like 'актив*') -} - -$cfg = Get-Config -Path $ConfigPath -$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME } -$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port -$bucketId = 'aw-worktime-sessions_' + $hostValue -$pulse = 120 -$sleepSec = if ($PollSeconds -gt 0) { - $PollSeconds -} -elseif ($cfg.collector -and $cfg.collector.pollSeconds) { - [int]$cfg.collector.pollSeconds -} -else { - 30 -} - -Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue - -while ($true) { - $now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ') - $records = Get-SessionRecords - if (-not $records -or $records.Count -eq 0) { - $records = @([pscustomobject]@{ - username = $env:USERNAME - sessionName = '' - sessionId = (Get-Process -Id $PID).SessionId - state = 'Unknown' - }) - } - - foreach ($rec in $records) { - $payload = @{ - timestamp = $now - duration = 0 - data = @{ - username = [string]$rec.username - userId = "$($env:USERDOMAIN)\$($rec.username)" - sessionId = [int]$rec.sessionId - sessionName = [string]$rec.sessionName - state = [string]$rec.state - active = (Test-SessionIsActive -State ([string]$rec.state)) - hostname = $hostValue - source = 'worktime-session-collector' - } - } | ConvertTo-Json -Depth 6 -Compress - - try { - Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload - } - catch { - } - } - - Start-Sleep -Seconds $sleepSec -} -; } - } - - Start-Sleep -Seconds $sleepSec -}