@wenathlan/extension 1.1.38 → 1.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.js +216 -4
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +49 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +17 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +22 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +116 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +761 -10
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +14 -2
- package/extension/dist/pagebridge.js.map +3 -3
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +31 -2
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +169 -2
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -619,23 +619,136 @@ var sessionmemory = class {
|
|
|
619
619
|
async getexports() {
|
|
620
620
|
return await this.adapter.get("exports") ?? [];
|
|
621
621
|
}
|
|
622
|
+
/** Removes one exported data artifact by id and reports whether it existed. */
|
|
623
|
+
async removeexport(id) {
|
|
624
|
+
const records = await this.getexports();
|
|
625
|
+
const remaining = records.filter((item) => item.id !== id);
|
|
626
|
+
await this.adapter.set("exports", remaining);
|
|
627
|
+
return remaining.length !== records.length;
|
|
628
|
+
}
|
|
629
|
+
/** Removes one run store artifact by id and reports whether it existed. */
|
|
630
|
+
async removeartifact(id) {
|
|
631
|
+
const records = await this.getartifacts();
|
|
632
|
+
const remaining = records.filter((item) => item.id !== id);
|
|
633
|
+
await this.adapter.set("artifacts", remaining);
|
|
634
|
+
return remaining.length !== records.length;
|
|
635
|
+
}
|
|
636
|
+
/** Stores one batch download file record with its state, path and checksum, replacing the previous record of that id. */
|
|
637
|
+
async setdownload(record2) {
|
|
638
|
+
const records = (await this.adapter.get("downloads") ?? []).filter((item) => item.id !== record2.id);
|
|
639
|
+
await this.adapter.set("downloads", [record2, ...records]);
|
|
640
|
+
}
|
|
641
|
+
/** Returns every batch download file record with its state, path and checksum, newest first. */
|
|
642
|
+
async getdownloads() {
|
|
643
|
+
return await this.adapter.get("downloads") ?? [];
|
|
644
|
+
}
|
|
645
|
+
/** Records one captured network log record; netlog retention is a user setting and an absent value keeps every record. */
|
|
646
|
+
async addnetlog(record2) {
|
|
647
|
+
const records = await this.getnetlog();
|
|
648
|
+
const combined = [record2, ...records];
|
|
649
|
+
const retention = (await this.getsettings())?.netlogretention;
|
|
650
|
+
await this.adapter.set("netlog", retention === void 0 ? combined : combined.slice(0, retention));
|
|
651
|
+
}
|
|
652
|
+
/** Returns the captured network log of the run with its step correlation, newest first. */
|
|
653
|
+
async getnetlog() {
|
|
654
|
+
return await this.adapter.get("netlog") ?? [];
|
|
655
|
+
}
|
|
656
|
+
/** Stores one clipboard consent record with its prompt and origin, replacing the previous record of that id. */
|
|
657
|
+
async setclipconsent(record2) {
|
|
658
|
+
const records = (await this.adapter.get("clipconsents") ?? []).filter((item) => item.id !== record2.id);
|
|
659
|
+
await this.adapter.set("clipconsents", [record2, ...records]);
|
|
660
|
+
}
|
|
661
|
+
/** Returns every clipboard consent record with its prompt and origin, newest first. */
|
|
662
|
+
async getclipconsents() {
|
|
663
|
+
return await this.adapter.get("clipconsents") ?? [];
|
|
664
|
+
}
|
|
665
|
+
/** Records one clipboard entry hash with its origin provenance; the payload text itself never persists. */
|
|
666
|
+
async addclip(entry) {
|
|
667
|
+
const records = await this.getclips();
|
|
668
|
+
await this.adapter.set("clips", [entry, ...records]);
|
|
669
|
+
}
|
|
670
|
+
/** Returns every clipboard entry hash with its kind and origin provenance, newest first. */
|
|
671
|
+
async getclips() {
|
|
672
|
+
return await this.adapter.get("clips") ?? [];
|
|
673
|
+
}
|
|
674
|
+
/** Stores one quarantine entry with its scan verdict, replacing the previous entry of that id. */
|
|
675
|
+
async setquarantine(entry) {
|
|
676
|
+
const records = (await this.adapter.get("quarantines") ?? []).filter((item) => item.id !== entry.id);
|
|
677
|
+
await this.adapter.set("quarantines", [entry, ...records]);
|
|
678
|
+
}
|
|
679
|
+
/** Returns every quarantine entry with its scan verdict and release ref, newest first. */
|
|
680
|
+
async getquarantines() {
|
|
681
|
+
return await this.adapter.get("quarantines") ?? [];
|
|
682
|
+
}
|
|
683
|
+
/** Stores the reviewed cleanup rule set of the run, replacing the previous set. */
|
|
684
|
+
async setcleanuprules(rules) {
|
|
685
|
+
return this.adapter.set("cleanuprules", rules);
|
|
686
|
+
}
|
|
687
|
+
/** Returns the reviewed cleanup rule set of the run. */
|
|
688
|
+
async getcleanuprules() {
|
|
689
|
+
return await this.adapter.get("cleanuprules") ?? [];
|
|
690
|
+
}
|
|
691
|
+
/** Records one cleanup run in the run history. */
|
|
692
|
+
async addcleanuprun(run) {
|
|
693
|
+
const records = await this.getcleanupruns();
|
|
694
|
+
await this.adapter.set("cleanupruns", [run, ...records]);
|
|
695
|
+
}
|
|
696
|
+
/** Returns every cleanup run history record with removed and kept counts, newest first. */
|
|
697
|
+
async getcleanupruns() {
|
|
698
|
+
return await this.adapter.get("cleanupruns") ?? [];
|
|
699
|
+
}
|
|
700
|
+
/** Stores the capture naming counters of one task, replacing the previous counters of that task. */
|
|
701
|
+
async setcapturecounter(counter) {
|
|
702
|
+
const records = (await this.adapter.get("capturecounters") ?? []).filter((item) => item.taskid !== counter.taskid);
|
|
703
|
+
await this.adapter.set("capturecounters", [counter, ...records]);
|
|
704
|
+
}
|
|
705
|
+
/** Returns every stored capture naming counter per task, newest first. */
|
|
706
|
+
async getcapturecounters() {
|
|
707
|
+
return await this.adapter.get("capturecounters") ?? [];
|
|
708
|
+
}
|
|
709
|
+
/** Replaces the artifact inventory the cleanup sweeper plans against. */
|
|
710
|
+
async setinventory(entries) {
|
|
711
|
+
return this.adapter.set("inventory", entries);
|
|
712
|
+
}
|
|
713
|
+
/** Returns the artifact inventory with sizes and ages for the cleanup sweeper. */
|
|
714
|
+
async getinventory() {
|
|
715
|
+
return await this.adapter.get("inventory") ?? [];
|
|
716
|
+
}
|
|
717
|
+
/** Stores one user configured virus scanning hook, replacing the previous hook of that scanner name. */
|
|
718
|
+
async setscanhook(config) {
|
|
719
|
+
const records = (await this.adapter.get("scanhooks") ?? []).filter((item) => item.scanner !== config.scanner);
|
|
720
|
+
await this.adapter.set("scanhooks", [config, ...records]);
|
|
721
|
+
}
|
|
722
|
+
/** Returns every configured virus scanning hook, newest first. */
|
|
723
|
+
async getscanhooks() {
|
|
724
|
+
return await this.adapter.get("scanhooks") ?? [];
|
|
725
|
+
}
|
|
726
|
+
/** Stores the armed mime interception filters of the run, newest first. */
|
|
727
|
+
async setmimefilters(filters) {
|
|
728
|
+
return this.adapter.set("mimefilters", filters);
|
|
729
|
+
}
|
|
730
|
+
/** Returns the armed mime interception filters of the run, newest first. */
|
|
731
|
+
async getmimefilters() {
|
|
732
|
+
return await this.adapter.get("mimefilters") ?? [];
|
|
733
|
+
}
|
|
622
734
|
};
|
|
623
735
|
function randomid() {
|
|
624
736
|
return crypto.randomUUID();
|
|
625
737
|
}
|
|
626
738
|
|
|
627
739
|
// policy.ts
|
|
628
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract"]);
|
|
740
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "openlink", "openprivate", "reloadcache", "stopnav", "followlink", "spanav", "rewritequery", "setfragment", "navlist", "navprofile", "handleauth", "printpdf", "prefetch", "preconnect", "deeplink", "reopentab", "pausenav", "navrate", "openclipboard", "batchopen", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "restorelayout", "reopenrun", "badgetab", "fillform", "filllabel", "fillplaceholder", "submitform", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcard", "fillcode", "consentpassword", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk", "paginateextract", "resumeextract", "batchdownload", "pausedownload", "resumedownload", "interceptmime", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "cleanupartifacts"]);
|
|
629
741
|
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
630
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance"]);
|
|
742
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector", "waitload", "waiturl", "spawait", "detecthttp", "readredirects", "readfinalurl", "trailaudit", "navintent", "checksafe", "querytabs", "watchtab", "findclones", "searchtabs", "listaudio", "snapshotsession", "savelayout", "attachmeta", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "readerrors", "skiphoneypot", "detectlogin", "detecttemplate", "handoffcaptcha", "scrapetable", "importcsv", "looprows", "transformvalues", "deduperows", "mergepages", "stamplerows", "previewgrid", "logprovenance", "verifydownload", "exportnetlog", "namecaptures"]);
|
|
631
743
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
632
744
|
var watchactions = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "watchtab"]);
|
|
633
745
|
var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow", "deriveselector", "fingerprintsection", "submitform", "retryform", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "scrapetable", "paginateextract"]);
|
|
634
|
-
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword"]);
|
|
746
|
+
var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor", "followlink", "setfragment", "handleauth", "navintent", "openclipboard", "checksafe", "reopentab", "spanav", "duplicatetab", "pintab", "mutetab", "movetab", "movetabwindow", "searchtabs", "badgetab", "attachmeta", "focuswindow", "maximizewindow", "minimizewindow", "restorewindow", "incognitowindow", "asksubmit", "selectchain", "picktypeahead", "pickdate", "attachfile", "fillcode", "consentpassword", "pausedownload", "resumedownload", "verifydownload", "writeclipboard", "quarantinedownload", "scanvirus"]);
|
|
635
747
|
var tabscommandactions = /* @__PURE__ */ new Set(["querytabs", "duplicatetab", "closepattern", "pintab", "mutetab", "movetab", "movetabwindow", "grouptabs", "colorgroup", "collapsegroup", "discardtab", "reloadtabs", "zoomin", "zoomout", "watchtab", "switchtab", "maximizewindow", "minimizewindow", "restorewindow", "focuswindow", "scratchwindow", "incognitowindow", "restoretab", "savelayout", "restorelayout", "findclones", "searchtabs", "badgetab", "attachmeta", "listaudio", "reopenrun", "snapshotsession"]);
|
|
636
748
|
var formactions = /* @__PURE__ */ new Set(["fillform", "filllabel", "fillplaceholder", "detectfields", "generatevalues", "saveprofiles", "asksubmit", "submitform", "readerrors", "retryform", "runwizard", "selectchain", "picktypeahead", "pickdate", "attachfile", "handoffcaptcha", "fillcard", "fillcode", "consentpassword", "skiphoneypot", "detectlogin", "detecttemplate"]);
|
|
637
749
|
var datasetactions = /* @__PURE__ */ new Set(["scrapetable", "exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "importcsv", "looprows", "transformvalues", "deduperows", "paginateextract", "mergepages", "stamplerows", "previewgrid", "streamdisk", "resumeextract", "logprovenance"]);
|
|
638
750
|
var exportactions = /* @__PURE__ */ new Set(["exportcsv", "exportjson", "exportexcel", "copytable", "pushsheets", "streamdisk"]);
|
|
751
|
+
var filesactions = /* @__PURE__ */ new Set(["batchdownload", "pausedownload", "resumedownload", "verifydownload", "interceptmime", "exportnetlog", "readclipboard", "writeclipboard", "copyscreen", "quarantinedownload", "scanvirus", "namecaptures", "cleanupartifacts"]);
|
|
639
752
|
var fieldkinds = ["text", "email", "phone", "date", "number", "select", "check", "radio", "file", "password", "card", "code"];
|
|
640
753
|
var layoutmutationactions = /* @__PURE__ */ new Set(["grouptabs", "colorgroup", "collapsegroup", "savelayout", "restorelayout"]);
|
|
641
754
|
var groupcolors = ["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"];
|
|
@@ -671,6 +784,9 @@ function requiredcapability(kind) {
|
|
|
671
784
|
if (kind === "downloadfile") return "downloads";
|
|
672
785
|
if (kind === "openclipboard") return "clipboardRead";
|
|
673
786
|
if (kind === "copytable") return "clipboardWrite";
|
|
787
|
+
if (kind === "batchdownload" || kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "interceptmime" || kind === "quarantinedownload" || kind === "scanvirus") return "downloads";
|
|
788
|
+
if (kind === "readclipboard") return "clipboardRead";
|
|
789
|
+
if (kind === "writeclipboard" || kind === "copyscreen") return "clipboardWrite";
|
|
674
790
|
if (kind === "openlink" || kind === "openprivate" || kind === "navlist" || kind === "batchopen" || kind === "reopentab" || kind === "deeplink") return "tabs";
|
|
675
791
|
if (tabscommandactions.has(kind)) return "tabs";
|
|
676
792
|
return void 0;
|
|
@@ -690,6 +806,9 @@ function isdatasetkind(kind) {
|
|
|
690
806
|
function isexportkind(kind) {
|
|
691
807
|
return exportactions.has(kind);
|
|
692
808
|
}
|
|
809
|
+
function isfileskind(kind) {
|
|
810
|
+
return filesactions.has(kind);
|
|
811
|
+
}
|
|
693
812
|
function exportgranted(session, origin) {
|
|
694
813
|
if (!origingranted(session, origin)) return { allowed: false, reason: `The export of extracted data from ${origin} needs the session origin grants before it leaves local memory.` };
|
|
695
814
|
return { allowed: true };
|
|
@@ -874,6 +993,88 @@ function validatedatagrammar(step, options, origin) {
|
|
|
874
993
|
if (kind === "logprovenance" && !isnonempty(options.artifact)) return { allowed: false, reason: "A reviewed artifact id or name is required in options." };
|
|
875
994
|
return { allowed: true };
|
|
876
995
|
}
|
|
996
|
+
function validatedownloadspec(value) {
|
|
997
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed downloadspec with a url list is required in options." };
|
|
998
|
+
const spec = value;
|
|
999
|
+
if (!Array.isArray(spec.urls) || spec.urls.length === 0 || !spec.urls.every((url) => ishttpsurl(url))) return { allowed: false, reason: "The reviewed downloadspec needs a non-empty list of HTTPS urls." };
|
|
1000
|
+
if (spec.filename !== void 0 && !isnonempty(spec.filename)) return { allowed: false, reason: "The reviewed downloadspec filename rule must be a non-empty string." };
|
|
1001
|
+
if (spec.complete !== void 0 && spec.complete !== "size" && spec.complete !== "checksum") return { allowed: false, reason: "The reviewed downloadspec completion criterion must be size or checksum." };
|
|
1002
|
+
return { allowed: true };
|
|
1003
|
+
}
|
|
1004
|
+
function validatemimefilter(value) {
|
|
1005
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed mimefilter with include and exclude patterns is required in options." };
|
|
1006
|
+
const filter = value;
|
|
1007
|
+
if (!Array.isArray(filter.include) || filter.include.length === 0 || !filter.include.every((pattern) => isnonempty(pattern))) return { allowed: false, reason: "The reviewed mimefilter needs a non-empty list of include patterns." };
|
|
1008
|
+
if (filter.exclude !== void 0 && (!Array.isArray(filter.exclude) || !filter.exclude.every((pattern) => isnonempty(pattern)))) return { allowed: false, reason: "The reviewed mimefilter exclude patterns must be a list of non-empty strings." };
|
|
1009
|
+
if (filter.default !== "deny" && filter.default !== "allow") return { allowed: false, reason: "The reviewed mimefilter needs the deny or allow default for unlisted mime types." };
|
|
1010
|
+
return { allowed: true };
|
|
1011
|
+
}
|
|
1012
|
+
function validatecleanuprule(value) {
|
|
1013
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { allowed: false, reason: "A reviewed cleanuprule with an age, a kind and a keep policy is required." };
|
|
1014
|
+
const rule = value;
|
|
1015
|
+
if (typeof rule.age !== "number" || !Number.isFinite(rule.age) || rule.age <= 0) return { allowed: false, reason: "The reviewed cleanup age window must be a positive number of milliseconds with no code ceiling." };
|
|
1016
|
+
if (!isnonempty(rule.kind)) return { allowed: false, reason: "The reviewed cleanup rule needs a non-empty artifact kind, or any to match every kind." };
|
|
1017
|
+
if (rule.keep !== "none" && rule.keep !== "latest" && rule.keep !== "all") return { allowed: false, reason: "The reviewed cleanup keep policy must be none, latest or all." };
|
|
1018
|
+
return { allowed: true };
|
|
1019
|
+
}
|
|
1020
|
+
function validatefilesgrammar(step, options) {
|
|
1021
|
+
const kind = step.kind;
|
|
1022
|
+
if (kind === "batchdownload") {
|
|
1023
|
+
const speccheck = validatedownloadspec(options.downloadspec);
|
|
1024
|
+
if (!speccheck.allowed) return speccheck;
|
|
1025
|
+
if (options.concurrent !== void 0 && (typeof options.concurrent !== "number" || !Number.isInteger(options.concurrent) || options.concurrent < 1)) return { allowed: false, reason: "The reviewed concurrent download window must be a positive integer with no code ceiling." };
|
|
1026
|
+
}
|
|
1027
|
+
if (kind === "pausedownload" || kind === "resumedownload" || kind === "verifydownload" || kind === "quarantinedownload" || kind === "scanvirus") {
|
|
1028
|
+
if (!isnonempty(step.value)) return { allowed: false, reason: "A reviewed download or quarantine reference is required." };
|
|
1029
|
+
if (kind === "verifydownload") {
|
|
1030
|
+
if (options.checksum !== void 0 && !isnonempty(options.checksum)) return { allowed: false, reason: "The reviewed expected checksum must be a non-empty string." };
|
|
1031
|
+
if (options.bytes !== void 0 && (typeof options.bytes !== "number" || !Number.isFinite(options.bytes) || options.bytes < 0)) return { allowed: false, reason: "The reviewed expected size must be zero or a positive number of bytes." };
|
|
1032
|
+
}
|
|
1033
|
+
if (kind === "scanvirus" && options.scanner !== void 0 && !isnonempty(options.scanner)) return { allowed: false, reason: "The reviewed scanner name must be a non-empty string." };
|
|
1034
|
+
if (kind === "quarantinedownload" && options.reason !== void 0 && !isnonempty(options.reason)) return { allowed: false, reason: "The reviewed quarantine reason must be a non-empty string." };
|
|
1035
|
+
}
|
|
1036
|
+
if (kind === "interceptmime") {
|
|
1037
|
+
const filtercheck = validatemimefilter(options.mimefilter);
|
|
1038
|
+
if (!filtercheck.allowed) return filtercheck;
|
|
1039
|
+
}
|
|
1040
|
+
if (kind === "readclipboard") {
|
|
1041
|
+
if (!isnonempty(options.consentref)) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref of an approved consent prompt in options." };
|
|
1042
|
+
if (options.prompt !== void 0 && !isnonempty(options.prompt)) return { allowed: false, reason: "The reviewed clipboard consent prompt must be a non-empty string." };
|
|
1043
|
+
}
|
|
1044
|
+
if (kind === "exportnetlog" && options.stepid !== void 0 && !isnonempty(options.stepid)) return { allowed: false, reason: "The reviewed netlog step filter must be a non-empty step id." };
|
|
1045
|
+
if (kind === "namecaptures") {
|
|
1046
|
+
if (!isnonempty(options.task)) return { allowed: false, reason: "A reviewed task id is required in options for capture naming." };
|
|
1047
|
+
if (options.steps !== void 0 && (!Array.isArray(options.steps) || options.steps.length === 0 || !options.steps.every((item) => isnonempty(item)))) return { allowed: false, reason: "The reviewed capture steps must be a non-empty list of step ids when present." };
|
|
1048
|
+
if (options.extension !== void 0 && !isnonempty(options.extension)) return { allowed: false, reason: "The reviewed capture extension must be a non-empty string." };
|
|
1049
|
+
}
|
|
1050
|
+
if (kind === "cleanupartifacts" && options.rules !== void 0) {
|
|
1051
|
+
const rules = options.rules;
|
|
1052
|
+
if (!Array.isArray(rules) || rules.length === 0) return { allowed: false, reason: "The reviewed cleanup rules must be a non-empty list when present." };
|
|
1053
|
+
for (const item of rules) {
|
|
1054
|
+
const rulecheck = validatecleanuprule(item);
|
|
1055
|
+
if (!rulecheck.allowed) return rulecheck;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return { allowed: true };
|
|
1059
|
+
}
|
|
1060
|
+
function clipboardconsentgranted(step) {
|
|
1061
|
+
let options = {};
|
|
1062
|
+
try {
|
|
1063
|
+
options = parseoptions(step);
|
|
1064
|
+
} catch {
|
|
1065
|
+
options = {};
|
|
1066
|
+
}
|
|
1067
|
+
const consentref = options.consentref;
|
|
1068
|
+
if (typeof consentref !== "string" || !consentref.trim()) return { allowed: false, reason: "A clipboard read requires a reviewed consent ref in options." };
|
|
1069
|
+
return { allowed: true };
|
|
1070
|
+
}
|
|
1071
|
+
function quarantinereleasegranted(entry) {
|
|
1072
|
+
if (entry.scan !== "clean") return { allowed: false, reason: `The quarantined file ${entry.path} cannot leave quarantine with the ${entry.scan} scan verdict; only a clean verdict releases it.` };
|
|
1073
|
+
return { allowed: true };
|
|
1074
|
+
}
|
|
1075
|
+
function maskclipboard(payload) {
|
|
1076
|
+
return `[clipboard payload of ${payload.length} character${payload.length === 1 ? "" : "s"}]`;
|
|
1077
|
+
}
|
|
877
1078
|
function submitreviewgranted(steps, submitid) {
|
|
878
1079
|
const position = steps.findIndex((candidate) => candidate.id === submitid);
|
|
879
1080
|
const asked = steps.some((candidate, index) => candidate.kind === "asksubmit" && (position === -1 || index < position));
|
|
@@ -1400,6 +1601,10 @@ function validatestep(step, origin) {
|
|
|
1400
1601
|
const datacheck = validatedatagrammar(step, options, origin);
|
|
1401
1602
|
if (!datacheck.allowed) return datacheck;
|
|
1402
1603
|
}
|
|
1604
|
+
if (isfileskind(step.kind)) {
|
|
1605
|
+
const filescheck = validatefilesgrammar(step, options);
|
|
1606
|
+
if (!filescheck.allowed) return filescheck;
|
|
1607
|
+
}
|
|
1403
1608
|
if (step.kind === "tabcreate") {
|
|
1404
1609
|
if (options.background !== void 0 && typeof options.background !== "boolean") return { allowed: false, reason: "The reviewed background flag must be a boolean." };
|
|
1405
1610
|
if (options.window !== void 0 && (typeof options.window !== "number" || !Number.isInteger(options.window) || options.window < 0)) return { allowed: false, reason: "The reviewed target window id must be a non-negative integer." };
|
|
@@ -1454,6 +1659,11 @@ function canexecute(input) {
|
|
|
1454
1659
|
const consentgate = passwordconsentgranted(input.step);
|
|
1455
1660
|
if (!consentgate.allowed) return consentgate;
|
|
1456
1661
|
}
|
|
1662
|
+
if (input.step.kind === "readclipboard") {
|
|
1663
|
+
const clipgate = clipboardconsentgranted(input.step);
|
|
1664
|
+
if (!clipgate.allowed) return clipgate;
|
|
1665
|
+
}
|
|
1666
|
+
if (input.step.kind === "interceptmime" && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The download interception is outside the session origin grants." };
|
|
1457
1667
|
if (input.step.kind === "openlink" || input.step.kind === "openprivate" || input.step.kind === "batchopen" || input.step.kind === "prefetch" || input.step.kind === "deeplink" || input.step.kind === "reopentab") {
|
|
1458
1668
|
let options = {};
|
|
1459
1669
|
try {
|
|
@@ -1555,9 +1765,18 @@ function recordwizardstep(progress, planid, stepid, state, now) {
|
|
|
1555
1765
|
const outcome = { stepid, ok: done, summary: `Wizard step ${executed} of ${state.steps} ${done ? "completed the wizard" : "executed"}.`, details: { wizard: { index: state.index, steps: state.steps, completed: [...state.completed] } }, at: now };
|
|
1556
1766
|
return recordoutcome(base, planid, outcome, now);
|
|
1557
1767
|
}
|
|
1768
|
+
function downloadshare(completed, total) {
|
|
1769
|
+
if (!Number.isFinite(total) || total <= 0) return 0;
|
|
1770
|
+
return Math.min(1, Math.max(0, completed) / total);
|
|
1771
|
+
}
|
|
1772
|
+
function recorddownload(progress, planid, stepid, entry, now) {
|
|
1773
|
+
const base = progress && progress.planid === planid ? progress : emptyprogress(planid, now);
|
|
1774
|
+
const outcome = { stepid, ok: entry.state === "complete", summary: `Download ${entry.index + 1} of ${entry.url} ended in the ${entry.state} state.`, details: { download: entry }, at: now };
|
|
1775
|
+
return recordoutcome(base, planid, outcome, now);
|
|
1776
|
+
}
|
|
1558
1777
|
|
|
1559
1778
|
// version.ts
|
|
1560
|
-
var packageversion = "1.1.
|
|
1779
|
+
var packageversion = "1.1.39";
|
|
1561
1780
|
|
|
1562
1781
|
// types.ts
|
|
1563
1782
|
var protocolversion = packageversion;
|
|
@@ -1674,6 +1893,15 @@ function extractionreport(input) {
|
|
|
1674
1893
|
function provenancereport(input) {
|
|
1675
1894
|
return { version: protocolversion, records: input.records };
|
|
1676
1895
|
}
|
|
1896
|
+
function downloadreport(input) {
|
|
1897
|
+
return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, downloads: input.downloads });
|
|
1898
|
+
}
|
|
1899
|
+
function netlogreport(input) {
|
|
1900
|
+
return { version: protocolversion, records: input.records };
|
|
1901
|
+
}
|
|
1902
|
+
function quarantinereport(input) {
|
|
1903
|
+
return { version: protocolversion, entries: input.entries };
|
|
1904
|
+
}
|
|
1677
1905
|
|
|
1678
1906
|
// extension/browsertabs.ts
|
|
1679
1907
|
var browserkinds = /* @__PURE__ */ new Set(["tablist", "tabcreate", "tabactivate", "tabclose", "tabreload", "tabsnapshot", "windowlist", "windowcreate", "windowclose", "zoomset", "windowresize", "downloadfile"]);
|
|
@@ -2002,13 +2230,13 @@ function presshold(holds, hold) {
|
|
|
2002
2230
|
return { holds: [...holds, hold], ok: true };
|
|
2003
2231
|
}
|
|
2004
2232
|
function releasehold(holds, holdid, releasedat) {
|
|
2005
|
-
let
|
|
2233
|
+
let released2;
|
|
2006
2234
|
const next = holds.map((hold) => {
|
|
2007
2235
|
if (hold.holdid !== holdid || hold.releasedat !== void 0) return hold;
|
|
2008
|
-
|
|
2009
|
-
return
|
|
2236
|
+
released2 = { ...hold, releasedat };
|
|
2237
|
+
return released2;
|
|
2010
2238
|
});
|
|
2011
|
-
return { holds: next, ...
|
|
2239
|
+
return { holds: next, ...released2 ? { released: released2 } : {} };
|
|
2012
2240
|
}
|
|
2013
2241
|
function heldkeys(holds, tabid2) {
|
|
2014
2242
|
return holds.filter((hold) => hold.releasedat === void 0 && (tabid2 === void 0 || hold.tabid === void 0 || hold.tabid === tabid2));
|
|
@@ -2691,6 +2919,153 @@ function mergetaskrules(existing, taskid, transforms, dedupekeys, at) {
|
|
|
2691
2919
|
};
|
|
2692
2920
|
}
|
|
2693
2921
|
|
|
2922
|
+
// extension/filescommand.ts
|
|
2923
|
+
var downloadtransitions = {
|
|
2924
|
+
queued: ["running", "complete", "failed"],
|
|
2925
|
+
running: ["paused", "complete", "failed"],
|
|
2926
|
+
paused: ["running", "failed"],
|
|
2927
|
+
complete: [],
|
|
2928
|
+
failed: []
|
|
2929
|
+
};
|
|
2930
|
+
function transitionallowed(from, to) {
|
|
2931
|
+
return downloadtransitions[from].includes(to);
|
|
2932
|
+
}
|
|
2933
|
+
function advancedownload(record2, state, at, evidence) {
|
|
2934
|
+
if (!transitionallowed(record2.state, state)) return record2;
|
|
2935
|
+
return {
|
|
2936
|
+
...record2,
|
|
2937
|
+
state,
|
|
2938
|
+
...evidence?.path !== void 0 ? { path: evidence.path } : record2.path !== void 0 ? { path: record2.path } : {},
|
|
2939
|
+
...evidence?.bytes !== void 0 ? { bytes: evidence.bytes } : record2.bytes !== void 0 ? { bytes: record2.bytes } : {},
|
|
2940
|
+
...evidence?.checksum !== void 0 ? { checksum: evidence.checksum } : record2.checksum !== void 0 ? { checksum: record2.checksum } : {},
|
|
2941
|
+
...evidence?.downloadid !== void 0 ? { downloadid: evidence.downloadid } : record2.downloadid !== void 0 ? { downloadid: record2.downloadid } : {},
|
|
2942
|
+
updatedat: at
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
function concurrentwindow(running, ceiling) {
|
|
2946
|
+
return ceiling === void 0 || running < ceiling;
|
|
2947
|
+
}
|
|
2948
|
+
function conflictfree(filename, taken) {
|
|
2949
|
+
if (!taken.includes(filename)) return filename;
|
|
2950
|
+
const dot = filename.lastIndexOf(".");
|
|
2951
|
+
const base = dot > 0 ? filename.slice(0, dot) : filename;
|
|
2952
|
+
const extension = dot > 0 ? filename.slice(dot) : "";
|
|
2953
|
+
let sequence = 2;
|
|
2954
|
+
while (taken.includes(`${base}-${sequence}${extension}`)) sequence += 1;
|
|
2955
|
+
return `${base}-${sequence}${extension}`;
|
|
2956
|
+
}
|
|
2957
|
+
function downloadfilename(url, rule) {
|
|
2958
|
+
if (rule && rule.trim()) return rule.trim();
|
|
2959
|
+
let name = "";
|
|
2960
|
+
try {
|
|
2961
|
+
const parsed = new URL(url);
|
|
2962
|
+
name = decodeURIComponent(parsed.pathname.split("/").filter(Boolean).pop() ?? parsed.hostname);
|
|
2963
|
+
} catch {
|
|
2964
|
+
name = url;
|
|
2965
|
+
}
|
|
2966
|
+
return name || "download";
|
|
2967
|
+
}
|
|
2968
|
+
function verifybytes(record2, expected) {
|
|
2969
|
+
const statematch = record2.state === "complete";
|
|
2970
|
+
const sizematch = expected.bytes === void 0 ? true : record2.bytes === expected.bytes;
|
|
2971
|
+
const checksummatch = expected.checksum === void 0 ? true : record2.checksum === expected.checksum;
|
|
2972
|
+
const ok = statematch && sizematch && checksummatch;
|
|
2973
|
+
const parts = [`state ${record2.state}${statematch ? " matches" : " does not match the completed expectation"}`];
|
|
2974
|
+
if (expected.bytes !== void 0) parts.push(`size ${record2.bytes ?? "unknown"} of ${expected.bytes} bytes ${sizematch ? "matches" : "differs"}`);
|
|
2975
|
+
if (expected.checksum !== void 0) parts.push(`checksum ${record2.checksum ?? "unknown"} ${checksummatch ? "matches" : "differs from"} the reviewed ${expected.checksum}`);
|
|
2976
|
+
return { ok, summary: `${ok ? "Verified" : "Failed to verify"} the download of ${record2.filename}: ${parts.join("; ")}.`, matches: { state: statematch, size: sizematch, checksum: checksummatch } };
|
|
2977
|
+
}
|
|
2978
|
+
function mimepatternmatches(pattern, mime) {
|
|
2979
|
+
if (!pattern.endsWith("*")) return pattern === mime;
|
|
2980
|
+
return mime.startsWith(pattern.slice(0, -1));
|
|
2981
|
+
}
|
|
2982
|
+
function mimeallowed(filter, mime) {
|
|
2983
|
+
if (filter.exclude.some((pattern) => mimepatternmatches(pattern, mime))) return false;
|
|
2984
|
+
if (filter.include.some((pattern) => mimepatternmatches(pattern, mime))) return true;
|
|
2985
|
+
return filter.default === "allow";
|
|
2986
|
+
}
|
|
2987
|
+
function redactheaders(headers) {
|
|
2988
|
+
return Object.fromEntries(Object.entries(headers).map(([name]) => [name, "[redacted]"]));
|
|
2989
|
+
}
|
|
2990
|
+
function netlogentry(input) {
|
|
2991
|
+
return { url: input.url, method: input.method, status: input.status, timing: input.timing, requestid: input.requestid, stepid: input.stepid, at: input.at };
|
|
2992
|
+
}
|
|
2993
|
+
function netlogforstep(records, stepid) {
|
|
2994
|
+
return records.filter((record2) => record2.stepid === stepid);
|
|
2995
|
+
}
|
|
2996
|
+
function clipentryof(kind, payload, origin, stepid, at) {
|
|
2997
|
+
return { kind, hash: payload.hash, length: payload.length, origin, stepid, at };
|
|
2998
|
+
}
|
|
2999
|
+
function cliphash(payload) {
|
|
3000
|
+
return checksum(payload);
|
|
3001
|
+
}
|
|
3002
|
+
function quarantinedpath(filename) {
|
|
3003
|
+
return `devthink-quarantine/${filename.replace(/^\/+/, "")}`;
|
|
3004
|
+
}
|
|
3005
|
+
function newquarantine(id, filename, reason, at) {
|
|
3006
|
+
const path = quarantinedpath(filename);
|
|
3007
|
+
return { id, path, reason, scan: "pending", at, updatedat: at };
|
|
3008
|
+
}
|
|
3009
|
+
function scanresult(entry, verdict, at) {
|
|
3010
|
+
return { ...entry, scan: verdict, updatedat: at };
|
|
3011
|
+
}
|
|
3012
|
+
function scanverdictof(response) {
|
|
3013
|
+
if (!response || typeof response !== "object") return "pending";
|
|
3014
|
+
const verdict = response.verdict;
|
|
3015
|
+
if (verdict === "clean" || verdict === "flagged" || verdict === "error") return verdict;
|
|
3016
|
+
return "pending";
|
|
3017
|
+
}
|
|
3018
|
+
function released(entry, ref, at) {
|
|
3019
|
+
return { ...entry, release: ref, updatedat: at };
|
|
3020
|
+
}
|
|
3021
|
+
function capturepart(value) {
|
|
3022
|
+
return value.replace(/[^a-z0-9-]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "capture";
|
|
3023
|
+
}
|
|
3024
|
+
function capturefilename(name, extension) {
|
|
3025
|
+
const safeextension = extension.replace(/^\.+/, "").toLowerCase() || "png";
|
|
3026
|
+
return `${capturepart(name.task)}-${capturepart(name.step)}-${name.sequence}.${safeextension}`;
|
|
3027
|
+
}
|
|
3028
|
+
function advancecounter(counters, base) {
|
|
3029
|
+
const sequence = (counters[base] ?? 0) + 1;
|
|
3030
|
+
return { sequence, counters: { ...counters, [base]: sequence } };
|
|
3031
|
+
}
|
|
3032
|
+
function capturenames(counters, task, steps, extension) {
|
|
3033
|
+
let current = { ...counters };
|
|
3034
|
+
const names = steps.map((step) => {
|
|
3035
|
+
const advanced = advancecounter(current, step);
|
|
3036
|
+
current = advanced.counters;
|
|
3037
|
+
return capturefilename({ task, step, sequence: advanced.sequence }, extension);
|
|
3038
|
+
});
|
|
3039
|
+
return { names, counters: current };
|
|
3040
|
+
}
|
|
3041
|
+
function referencedartifacts(plan, completed) {
|
|
3042
|
+
if (!plan) return [];
|
|
3043
|
+
return plan.steps.filter((step) => step.kind === "attachfile" && !completed.includes(step.id)).map((step) => step.value ?? "").filter((value) => value.trim().length > 0);
|
|
3044
|
+
}
|
|
3045
|
+
function sweepplan(entries, rules, now, keeprefs) {
|
|
3046
|
+
const remove = /* @__PURE__ */ new Set();
|
|
3047
|
+
for (const rule of rules) {
|
|
3048
|
+
const matching = entries.filter((entry) => rule.kind === "any" || entry.kind === rule.kind);
|
|
3049
|
+
const aged = matching.filter((entry) => now - entry.at >= rule.age);
|
|
3050
|
+
const kept = [];
|
|
3051
|
+
if (rule.keep === "all") kept.push(...aged);
|
|
3052
|
+
else if (rule.keep === "latest") {
|
|
3053
|
+
const newest = [...aged].sort((left, right) => right.at - left.at)[0];
|
|
3054
|
+
if (newest) kept.push(newest);
|
|
3055
|
+
}
|
|
3056
|
+
for (const entry of aged) {
|
|
3057
|
+
if (kept.some((item) => item.id === entry.id)) continue;
|
|
3058
|
+
if (keeprefs.includes(entry.id) || keeprefs.includes(entry.name)) continue;
|
|
3059
|
+
remove.add(entry.id);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
return { remove: [...remove], keep: entries.filter((entry) => !remove.has(entry.id)).map((entry) => entry.id) };
|
|
3063
|
+
}
|
|
3064
|
+
function capturesteps(options, plan) {
|
|
3065
|
+
const listed = Array.isArray(options.steps) ? options.steps.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
3066
|
+
return listed.length > 0 ? listed : plan.steps.map((step) => step.id);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
2694
3069
|
// extension/background.ts
|
|
2695
3070
|
var sessionduration = 15 * 60 * 1e3;
|
|
2696
3071
|
var freshcheckkinds = /* @__PURE__ */ new Set(["focus", "inspect", "click", "type", "scroll", "select", "hover"]);
|
|
@@ -2881,6 +3256,14 @@ function stepauditkind(step, ok) {
|
|
|
2881
3256
|
if (step.kind === "submitform" || step.kind === "asksubmit") return "submit";
|
|
2882
3257
|
if (step.kind === "consentpassword") return "consent";
|
|
2883
3258
|
if (step.kind === "handoffcaptcha") return "handoff";
|
|
3259
|
+
if (isfileskind(step.kind)) {
|
|
3260
|
+
if (step.kind === "interceptmime") return "intercept";
|
|
3261
|
+
if (step.kind === "readclipboard" || step.kind === "writeclipboard" || step.kind === "copyscreen") return "clipboard";
|
|
3262
|
+
if (step.kind === "quarantinedownload" || step.kind === "scanvirus") return "quarantine";
|
|
3263
|
+
if (step.kind === "cleanupartifacts") return "cleanup";
|
|
3264
|
+
if (step.kind === "verifydownload" || step.kind === "exportnetlog" || step.kind === "namecaptures") return "observation";
|
|
3265
|
+
return "download";
|
|
3266
|
+
}
|
|
2884
3267
|
if (isdatasetkind(step.kind)) {
|
|
2885
3268
|
if (step.kind === "exportcsv" || step.kind === "exportjson" || step.kind === "exportexcel" || step.kind === "copytable" || step.kind === "pushsheets") return "export";
|
|
2886
3269
|
if (step.kind === "streamdisk") return "stream";
|
|
@@ -3190,8 +3573,18 @@ async function recordnavigation(step, session, tabid2) {
|
|
|
3190
3573
|
await memory.addnavrecord(record2);
|
|
3191
3574
|
await memory.setnavstate(tabid2, record2);
|
|
3192
3575
|
if (session && url) await memory.addtrailentry(session.id, { url, title, stepid: step.id, at: Date.now() });
|
|
3576
|
+
await collectnetlog(tabid2, step);
|
|
3193
3577
|
return record2;
|
|
3194
3578
|
}
|
|
3579
|
+
async function collectnetlog(tabid2, step) {
|
|
3580
|
+
const events = navbuffers.get(tabid2) ?? [];
|
|
3581
|
+
if (events.length === 0) return;
|
|
3582
|
+
const first = events[0]?.timestamp ?? Date.now();
|
|
3583
|
+
for (const [index, event] of events.entries()) {
|
|
3584
|
+
const status = event.status ?? (event.event === "completed" ? 200 : event.event === "error" ? 0 : 0);
|
|
3585
|
+
await memory.addnetlog(netlogentry({ url: event.url, method: "GET", status, timing: Math.max(0, event.timestamp - first), requestid: `${step.id}-${index + 1}`, stepid: step.id, at: event.timestamp }));
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3195
3588
|
function injectallowedorigins(step, session) {
|
|
3196
3589
|
const allowedorigins = session?.grants ?? (session ? [session.origin] : []);
|
|
3197
3590
|
let options = {};
|
|
@@ -4284,6 +4677,270 @@ async function executedatastep(step, session, plan, tabid2, origin) {
|
|
|
4284
4677
|
return { ok: false, summary: "Unsupported forms and data step." };
|
|
4285
4678
|
}
|
|
4286
4679
|
}
|
|
4680
|
+
async function loadownload(reference) {
|
|
4681
|
+
const records = await memory.getdownloads();
|
|
4682
|
+
return records.find((item) => item.id === reference || item.filename === reference || item.url === reference);
|
|
4683
|
+
}
|
|
4684
|
+
async function settlesdownload(record2) {
|
|
4685
|
+
const started = Date.now();
|
|
4686
|
+
for (; ; ) {
|
|
4687
|
+
const items = record2.downloadid === void 0 ? [] : await chrome.downloads.search({ id: record2.downloadid }).catch(() => []);
|
|
4688
|
+
const item = items[0];
|
|
4689
|
+
if (item?.state === "complete") return { state: "complete", evidence: { path: item.filename, bytes: item.fileSize ?? item.totalBytes ?? 0 } };
|
|
4690
|
+
if (item?.state === "interrupted") return { state: "failed" };
|
|
4691
|
+
if (Date.now() - started >= evidencesettle) return { state: "running" };
|
|
4692
|
+
await new Promise((resolve) => setTimeout(resolve, evidencepoll));
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
async function pauseonerecord(record2, extra) {
|
|
4696
|
+
if (!transitionallowed(record2.state, "paused")) throw new Error(`A ${record2.state} download cannot pause.`);
|
|
4697
|
+
if (record2.downloadid !== void 0) await chrome.downloads.pause(record2.downloadid).catch(() => void 0);
|
|
4698
|
+
const paused = advancedownload(record2, "paused", Date.now());
|
|
4699
|
+
await memory.setdownload(paused);
|
|
4700
|
+
await audit("download", `Paused the download of ${record2.filename} from ${record2.url}.`, extra);
|
|
4701
|
+
return { ok: true, summary: `Paused the download of ${record2.filename}.`, details: { download: paused } };
|
|
4702
|
+
}
|
|
4703
|
+
async function resumeonerecord(record2, extra) {
|
|
4704
|
+
if (!transitionallowed(record2.state, "running")) throw new Error(`A ${record2.state} download cannot resume.`);
|
|
4705
|
+
if (record2.downloadid !== void 0) await chrome.downloads.resume(record2.downloadid).catch(() => void 0);
|
|
4706
|
+
const resumed = advancedownload(record2, "running", Date.now());
|
|
4707
|
+
await memory.setdownload(resumed);
|
|
4708
|
+
await audit("download", `Resumed the paused download of ${record2.filename} from ${record2.url}.`, extra);
|
|
4709
|
+
return { ok: true, summary: `Resumed the download of ${record2.filename}.`, details: { download: resumed } };
|
|
4710
|
+
}
|
|
4711
|
+
async function verifyonerecord(record2, expected, extra) {
|
|
4712
|
+
const verification = verifybytes(record2, expected);
|
|
4713
|
+
await audit("observation", `Verified the download of ${record2.filename}: ${verification.summary}`, extra);
|
|
4714
|
+
return { ok: verification.ok, summary: verification.summary, details: { verification: { ...verification.matches, state: record2.state, path: record2.path, checksum: record2.checksum, bytes: record2.bytes } } };
|
|
4715
|
+
}
|
|
4716
|
+
async function executefilesstep(step, session, plan, tabid2, origin) {
|
|
4717
|
+
const options = stepoptions2(step);
|
|
4718
|
+
const extra = { ...session ? { sessionid: session.id } : {}, planid: plan.id, stepid: step.id };
|
|
4719
|
+
switch (step.kind) {
|
|
4720
|
+
case "batchdownload": {
|
|
4721
|
+
const spec = options.downloadspec;
|
|
4722
|
+
const urls = Array.isArray(spec?.urls) ? (spec?.urls).filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
4723
|
+
const concurrent = typeof options.concurrent === "number" && Number.isInteger(options.concurrent) && options.concurrent > 0 ? options.concurrent : void 0;
|
|
4724
|
+
const taken = (await memory.getdownloads()).map((record2) => record2.filename);
|
|
4725
|
+
const records = urls.map((url) => {
|
|
4726
|
+
const filename = conflictfree(downloadfilename(url, spec?.filename), taken);
|
|
4727
|
+
taken.push(filename);
|
|
4728
|
+
return { id: randomid(), url, filename, state: "queued", at: Date.now(), updatedat: Date.now() };
|
|
4729
|
+
});
|
|
4730
|
+
let completed = 0;
|
|
4731
|
+
let failed = 0;
|
|
4732
|
+
let queued = 0;
|
|
4733
|
+
let index = 0;
|
|
4734
|
+
while (index < records.length) {
|
|
4735
|
+
const wave = [];
|
|
4736
|
+
while (index < records.length && concurrentwindow(wave.length, concurrent)) {
|
|
4737
|
+
const record2 = records[index];
|
|
4738
|
+
index += 1;
|
|
4739
|
+
const downloadid = await chrome.downloads.download({ url: record2.url, filename: record2.filename }).catch(() => void 0);
|
|
4740
|
+
const started = downloadid === void 0 ? advancedownload(record2, "failed", Date.now()) : advancedownload(record2, "running", Date.now(), { downloadid });
|
|
4741
|
+
await memory.setdownload(started);
|
|
4742
|
+
wave.push(started);
|
|
4743
|
+
}
|
|
4744
|
+
for (const started of wave) {
|
|
4745
|
+
if (started.state === "failed") {
|
|
4746
|
+
failed += 1;
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
const settled = await settlesdownload(started);
|
|
4750
|
+
const final = advancedownload(started, settled.state, Date.now(), settled.evidence);
|
|
4751
|
+
await memory.setdownload(final);
|
|
4752
|
+
if (final.state === "complete") completed += 1;
|
|
4753
|
+
else if (final.state === "failed") failed += 1;
|
|
4754
|
+
else queued += 1;
|
|
4755
|
+
await memory.setprogress(recorddownload(await memory.getprogress(), plan.id, step.id, { index: records.indexOf(started), url: started.url, state: final.state }, Date.now()));
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
await refreshbadge();
|
|
4759
|
+
await audit("download", `Batch downloaded ${records.length} reviewed file${records.length === 1 ? "" : "s"}${concurrent !== void 0 ? ` under the user configured concurrent window of ${concurrent}` : ""}: ${completed} completed, ${failed} failed${queued > 0 ? `, ${queued} still running` : ""}.`, extra);
|
|
4760
|
+
return { ok: failed === 0, summary: `Batch downloaded ${records.length} file${records.length === 1 ? "" : "s"}: ${completed} completed, ${failed} failed${queued > 0 ? `, ${queued} still running` : ""}.`, details: { downloads: records, completed, failed, running: queued, total: records.length, share: downloadshare(completed, records.length), concurrent } };
|
|
4761
|
+
}
|
|
4762
|
+
case "pausedownload": {
|
|
4763
|
+
const record2 = await loadownload(step.value ?? "");
|
|
4764
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
4765
|
+
return pauseonerecord(record2, extra);
|
|
4766
|
+
}
|
|
4767
|
+
case "resumedownload": {
|
|
4768
|
+
const record2 = await loadownload(step.value ?? "");
|
|
4769
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
4770
|
+
return resumeonerecord(record2, extra);
|
|
4771
|
+
}
|
|
4772
|
+
case "verifydownload": {
|
|
4773
|
+
const record2 = await loadownload(step.value ?? "");
|
|
4774
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
4775
|
+
return verifyonerecord(record2, { ...typeof options.bytes === "number" ? { bytes: options.bytes } : {}, ...typeof options.checksum === "string" ? { checksum: options.checksum } : {} }, extra);
|
|
4776
|
+
}
|
|
4777
|
+
case "interceptmime": {
|
|
4778
|
+
const filter = options.mimefilter;
|
|
4779
|
+
if (!filter) throw new Error("A reviewed mimefilter is required in options.");
|
|
4780
|
+
const filters = await memory.getmimefilters();
|
|
4781
|
+
await memory.setmimefilters([filter, ...filters]);
|
|
4782
|
+
armedmimefilter = filter;
|
|
4783
|
+
installmimelistener();
|
|
4784
|
+
await audit("intercept", `Armed the reviewed mime interception filter: include ${filter.include.join(", ")}, exclude ${filter.exclude.join(", ") || "none"} and the ${filter.default} default for unlisted mime types; matching downloads reroute into quarantine.`, extra);
|
|
4785
|
+
return { ok: true, summary: `Armed the mime interception filter with the ${filter.default} default for unlisted mime types.`, details: { mimefilter: filter } };
|
|
4786
|
+
}
|
|
4787
|
+
case "exportnetlog": {
|
|
4788
|
+
const records = await memory.getnetlog();
|
|
4789
|
+
const stepfilter = typeof options.stepid === "string" && options.stepid ? options.stepid : void 0;
|
|
4790
|
+
const filtered = (stepfilter ? netlogforstep(records, stepfilter) : records).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
|
|
4791
|
+
await audit("observation", `Exported ${filtered.length} netlog record${filtered.length === 1 ? "" : "s"} of the run${stepfilter ? ` correlated with step ${stepfilter}` : ""} with every header value redacted.`, extra);
|
|
4792
|
+
return { ok: true, summary: `Exported ${filtered.length} netlog record${filtered.length === 1 ? "" : "s"} with header values redacted.`, details: { netlog: filtered, count: filtered.length, redacted: true, redaction: "every header value is redacted from exported netlogs" } };
|
|
4793
|
+
}
|
|
4794
|
+
case "readclipboard": {
|
|
4795
|
+
const consentref = typeof options.consentref === "string" ? options.consentref : "";
|
|
4796
|
+
const consents = await memory.getclipconsents();
|
|
4797
|
+
const consent = consents.find((item) => item.id === consentref && item.approved === true && item.usedat === void 0);
|
|
4798
|
+
if (!consent) {
|
|
4799
|
+
const pending = { id: consentref || randomid(), prompt: typeof options.prompt === "string" && options.prompt ? options.prompt : step.summary, origin, stepid: step.id, at: Date.now() };
|
|
4800
|
+
await memory.setclipconsent(pending);
|
|
4801
|
+
await refreshbadge();
|
|
4802
|
+
await audit("clipboard", `Clipboard read consent prompt ${pending.id} opened for step ${step.id} on ${origin}; the read waits for the user approval and every read needs its own prompt.`, extra);
|
|
4803
|
+
return { ok: false, summary: `The clipboard read waits for your consent approval${consentref ? ` under ref ${consentref}` : ""}; approve it in the review panel and run the step again.`, details: { consent: { id: pending.id, prompt: pending.prompt, origin, stepid: step.id, approved: false } } };
|
|
4804
|
+
}
|
|
4805
|
+
const text2 = await navigator.clipboard.readText();
|
|
4806
|
+
const entry = clipentryof("read", { hash: cliphash(text2), length: text2.length }, origin, step.id, Date.now());
|
|
4807
|
+
await memory.addclip(entry);
|
|
4808
|
+
await memory.setclipconsent({ ...consent, usedat: Date.now() });
|
|
4809
|
+
await refreshbadge();
|
|
4810
|
+
await audit("clipboard", `Read ${entry.length} clipboard character${entry.length === 1 ? "" : "s"} on the approved consent ${consent.id} with payload hash ${entry.hash}; the payload text never appears in logs or memory.`, extra);
|
|
4811
|
+
return { ok: true, summary: `Read ${entry.length} clipboard character${entry.length === 1 ? "" : "s"} on the approved consent ${consent.id} with payload hash ${entry.hash}.`, details: { clip: entry, masked: maskclipboard(text2) } };
|
|
4812
|
+
}
|
|
4813
|
+
case "writeclipboard": {
|
|
4814
|
+
const output = await dispatchpagestep(step, tabid2, origin, plan);
|
|
4815
|
+
const hash = typeof output?.details?.hash === "string" ? output.details.hash : cliphash(step.value ?? "");
|
|
4816
|
+
const length = typeof output?.details?.length === "number" ? output.details.length : (step.value ?? "").length;
|
|
4817
|
+
const entry = clipentryof("write", { hash, length }, origin, step.id, Date.now());
|
|
4818
|
+
await memory.addclip(entry);
|
|
4819
|
+
await audit("clipboard", `Wrote ${length} reviewed character${length === 1 ? "" : "s"} to the clipboard with payload hash ${hash}; the payload text never appears in logs or memory.`, extra);
|
|
4820
|
+
return { ok: Boolean(output?.ok), summary: output?.summary ?? "The clipboard write returned no result.", details: { ...output?.details ?? {}, clip: entry } };
|
|
4821
|
+
}
|
|
4822
|
+
case "copyscreen": {
|
|
4823
|
+
const windowid = chrome.windows.WINDOW_ID_CURRENT;
|
|
4824
|
+
const shot = await chrome.tabs.captureVisibleTab(windowid, { format: "png" });
|
|
4825
|
+
let destination = "clipboard";
|
|
4826
|
+
try {
|
|
4827
|
+
const blob = await (await fetch(shot)).blob();
|
|
4828
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
4829
|
+
} catch {
|
|
4830
|
+
destination = "clipboard unavailable";
|
|
4831
|
+
}
|
|
4832
|
+
const entry = clipentryof("screen", { hash: cliphash(shot), length: shot.length }, origin, step.id, Date.now());
|
|
4833
|
+
await memory.addclip(entry);
|
|
4834
|
+
await audit("clipboard", `Copied a screenshot of the visible tab (${entry.length} characters of png data) to the clipboard with payload hash ${entry.hash}; destination ${destination}.`, extra);
|
|
4835
|
+
return { ok: destination === "clipboard", summary: `Copied the visible tab screenshot with payload hash ${entry.hash} to the clipboard.`, details: { clip: entry, destination } };
|
|
4836
|
+
}
|
|
4837
|
+
case "quarantinedownload": {
|
|
4838
|
+
const record2 = await loadownload(step.value ?? "");
|
|
4839
|
+
if (!record2) throw new Error(`No stored download matches ${step.value ?? ""}.`);
|
|
4840
|
+
const reason = typeof options.reason === "string" && options.reason ? options.reason : `moved from ${record2.url} by the reviewed quarantine step`;
|
|
4841
|
+
const entry = newquarantine(randomid(), record2.path ?? record2.filename, reason, Date.now());
|
|
4842
|
+
await memory.setquarantine(entry);
|
|
4843
|
+
await refreshbadge();
|
|
4844
|
+
await audit("quarantine", `Quarantined the download ${record2.filename} outside the downloads folder at ${entry.path} with reason ${reason}; the file stays there until a clean scan verdict releases it.`, extra);
|
|
4845
|
+
return { ok: true, summary: `Quarantined ${record2.filename} at ${entry.path} with a pending scan verdict.`, details: { quarantine: entry } };
|
|
4846
|
+
}
|
|
4847
|
+
case "scanvirus": {
|
|
4848
|
+
const reference = step.value ?? "";
|
|
4849
|
+
const entry = (await memory.getquarantines()).find((item) => item.id === reference || item.path === reference);
|
|
4850
|
+
if (!entry) throw new Error(`No quarantined file matches ${reference}.`);
|
|
4851
|
+
const scanner = typeof options.scanner === "string" && options.scanner ? options.scanner : void 0;
|
|
4852
|
+
const hooks = await memory.getscanhooks();
|
|
4853
|
+
const hook = scanner ? hooks.find((item) => item.scanner === scanner) : hooks[0];
|
|
4854
|
+
let verdict = "pending";
|
|
4855
|
+
if (hook) {
|
|
4856
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false);
|
|
4857
|
+
if (granted) {
|
|
4858
|
+
try {
|
|
4859
|
+
const response = await fetch(hook.endpoint, { method: "POST", headers: { "content-type": "application/json" }, credentials: "omit", body: JSON.stringify({ quarantine: entry.id, path: entry.path, reason: entry.reason }) });
|
|
4860
|
+
verdict = scanverdictof(await response.json().catch(() => void 0));
|
|
4861
|
+
} catch {
|
|
4862
|
+
verdict = "pending";
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
}
|
|
4866
|
+
const scanned = scanresult(entry, verdict, Date.now());
|
|
4867
|
+
await memory.setquarantine(scanned);
|
|
4868
|
+
await refreshbadge();
|
|
4869
|
+
await audit("quarantine", `Scan hook ${hook ? hook.scanner : scanner ?? "none configured"} ${hook ? `returned the ${verdict} verdict` : "is not configured or granted; the verdict stays pending"} for the quarantined file ${entry.path}; hook failures never release a file.`, extra);
|
|
4870
|
+
return { ok: true, summary: `Scan verdict ${verdict} recorded for ${entry.path}.`, details: { quarantine: scanned, verdict } };
|
|
4871
|
+
}
|
|
4872
|
+
case "namecaptures": {
|
|
4873
|
+
const task = typeof options.task === "string" && options.task ? options.task : plan.id;
|
|
4874
|
+
const steps = capturesteps(options, plan);
|
|
4875
|
+
const extension = typeof options.extension === "string" && options.extension ? options.extension : "png";
|
|
4876
|
+
const existing = (await memory.getcapturecounters()).find((item) => item.taskid === task);
|
|
4877
|
+
const stamped = capturenames(existing?.counters ?? {}, task, steps, extension);
|
|
4878
|
+
await memory.setcapturecounter({ taskid: task, counters: stamped.counters, at: Date.now() });
|
|
4879
|
+
await audit("observation", `Stamped ${stamped.names.length} consistent capture name${stamped.names.length === 1 ? "" : "s"} for task ${task} from task, step and sequence parts.`, extra);
|
|
4880
|
+
return { ok: true, summary: `Stamped ${stamped.names.length} capture name${stamped.names.length === 1 ? "" : "s"} for task ${task}.`, details: { task, names: stamped.names, counters: stamped.counters } };
|
|
4881
|
+
}
|
|
4882
|
+
case "cleanupartifacts": {
|
|
4883
|
+
const inlinrules = (Array.isArray(options.rules) ? options.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
4884
|
+
const rules = inlinrules.length > 0 ? inlinrules : await memory.getcleanuprules();
|
|
4885
|
+
if (rules.length === 0) throw new Error("No reviewed cleanup rules are present; pass rules in options or store a rule set from the review panel.");
|
|
4886
|
+
if (inlinrules.length > 0) await memory.setcleanuprules(inlinrules);
|
|
4887
|
+
const exportrecords = await memory.getexports();
|
|
4888
|
+
const artifacts = await memory.getartifacts();
|
|
4889
|
+
const inventory = [
|
|
4890
|
+
...exportrecords.map((artifact) => ({ id: artifact.id, kind: `export-${artifact.kind}`, name: artifact.name, size: artifact.content.length, at: artifact.at })),
|
|
4891
|
+
...artifacts.map((artifact) => ({ id: artifact.id, kind: artifact.kind, name: artifact.name, size: 0, at: artifact.at }))
|
|
4892
|
+
];
|
|
4893
|
+
await memory.setinventory(inventory);
|
|
4894
|
+
const progress = await memory.getprogress();
|
|
4895
|
+
const keeprefs = referencedartifacts(plan, progress?.planid === plan.id ? progress.completedsteps : []);
|
|
4896
|
+
const sweep = sweepplan(inventory, rules, Date.now(), keeprefs);
|
|
4897
|
+
let removed = 0;
|
|
4898
|
+
for (const id of sweep.remove) {
|
|
4899
|
+
if (await memory.removeexport(id)) removed += 1;
|
|
4900
|
+
else if (await memory.removeartifact(id)) removed += 1;
|
|
4901
|
+
}
|
|
4902
|
+
const run = { id: randomid(), rules: rules.length, removed, kept: sweep.keep.length, at: Date.now() };
|
|
4903
|
+
await memory.addcleanuprun(run);
|
|
4904
|
+
await refreshbadge();
|
|
4905
|
+
await audit("cleanup", `Cleanup sweep applied ${rules.length} reviewed rule${rules.length === 1 ? "" : "s"} by age and kind: removed ${removed} artifact${removed === 1 ? "" : "s"}, kept ${sweep.keep.length}${keeprefs.length > 0 ? ` while holding every artifact referenced by open review cards` : ""}.`, extra);
|
|
4906
|
+
return { ok: true, summary: `Cleanup sweep removed ${removed} artifact${removed === 1 ? "" : "s"} and kept ${sweep.keep.length} under the reviewed rules.`, details: { run: { id: run.id, rules: run.rules, removed: run.removed, kept: run.kept }, remove: sweep.remove, inventory: inventory.length } };
|
|
4907
|
+
}
|
|
4908
|
+
default:
|
|
4909
|
+
return { ok: false, summary: "Unsupported files, clipboard and downloads step." };
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
var armedmimefilter;
|
|
4913
|
+
var mimelistenerinstalled = false;
|
|
4914
|
+
function installmimelistener() {
|
|
4915
|
+
if (mimelistenerinstalled || typeof chrome.downloads?.onDeterminingFilename?.addListener !== "function") return;
|
|
4916
|
+
mimelistenerinstalled = true;
|
|
4917
|
+
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
4918
|
+
const filter = armedmimefilter;
|
|
4919
|
+
if (!filter || item.byExtensionId === chrome.runtime.id) {
|
|
4920
|
+
suggest({ filename: item.filename, conflictAction: "uniquify" });
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
const mime = item.mime ?? "";
|
|
4924
|
+
if (mimeallowed(filter, mime)) {
|
|
4925
|
+
suggest({ filename: `devthink-quarantine/${item.filename}`, conflictAction: "uniquify" });
|
|
4926
|
+
void memory.setquarantine(newquarantine(randomid(), item.filename, `mime ${mime || "unknown"} matched the reviewed include patterns`, Date.now())).then(() => audit("intercept", `Intercepted the download of ${item.filename} (${mime || "unknown mime"}) into quarantine under the reviewed mime filter.`, {})).catch(() => void 0);
|
|
4927
|
+
return;
|
|
4928
|
+
}
|
|
4929
|
+
if (filter.default === "deny") {
|
|
4930
|
+
void chrome.downloads.cancel(item.id).catch(() => void 0);
|
|
4931
|
+
void audit("intercept", `Denied the unlisted download of ${item.filename} (${mime || "unknown mime"}) under the deny default of the reviewed mime filter.`, {}).catch(() => void 0);
|
|
4932
|
+
}
|
|
4933
|
+
suggest({ filename: item.filename, conflictAction: "uniquify" });
|
|
4934
|
+
});
|
|
4935
|
+
}
|
|
4936
|
+
installmimelistener();
|
|
4937
|
+
async function reconcilmimefilter() {
|
|
4938
|
+
const filters = await memory.getmimefilters();
|
|
4939
|
+
armedmimefilter = filters[0];
|
|
4940
|
+
installmimelistener();
|
|
4941
|
+
}
|
|
4942
|
+
reconcilmimefilter().catch(() => {
|
|
4943
|
+
});
|
|
4287
4944
|
async function enforcewindowreview(step, session, plan) {
|
|
4288
4945
|
const windowid = step.value && /^\d+$/.test(step.value) ? Number.parseInt(step.value, 10) : 0;
|
|
4289
4946
|
const progress = plan ? await memory.getprogress() : void 0;
|
|
@@ -4319,9 +4976,11 @@ async function refreshbadge() {
|
|
|
4319
4976
|
const queues = await memory.getnavqueues();
|
|
4320
4977
|
const badges = await memory.getbadges();
|
|
4321
4978
|
const prompts = (await memory.gettickets()).filter((ticket) => ticket.approved === void 0).length;
|
|
4979
|
+
const consents = (await memory.getclipconsents()).filter((record2) => record2.approved === void 0).length;
|
|
4980
|
+
const quarantined = (await memory.getquarantines()).filter((entry) => entry.scan === "pending").length;
|
|
4322
4981
|
const datasets = (await memory.getdatasets()).length;
|
|
4323
4982
|
const tasktabs2 = new Set(badges.map((badge) => badge.tabid)).size;
|
|
4324
|
-
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + datasets;
|
|
4983
|
+
const total = (queues?.prefetch ?? 0) + (queues?.batchopen ?? 0) + tasktabs2 + prompts + consents + quarantined + datasets;
|
|
4325
4984
|
await chrome.action.setBadgeText({ text: total > 0 ? String(total) : "" }).catch(() => {
|
|
4326
4985
|
});
|
|
4327
4986
|
}
|
|
@@ -4347,6 +5006,8 @@ async function executestep(stepid) {
|
|
|
4347
5006
|
output = await executetabscommand(step, session, plan, tab.id);
|
|
4348
5007
|
} else if (isdatasetkind(step.kind)) {
|
|
4349
5008
|
output = await executedatastep(step, session, plan, tab.id, origin);
|
|
5009
|
+
} else if (isfileskind(step.kind)) {
|
|
5010
|
+
output = await executefilesstep(step, session, plan, tab.id, origin);
|
|
4350
5011
|
} else if (isformkind(step.kind)) {
|
|
4351
5012
|
output = await executeformstep(step, session, plan, tab.id, origin);
|
|
4352
5013
|
} else if (isbrowserkind(step.kind)) {
|
|
@@ -4451,6 +5112,7 @@ async function grantcapability(permission) {
|
|
|
4451
5112
|
if (!["tabs", "downloads", "clipboardRead", "clipboardWrite"].includes(permission)) throw new Error("Unknown capability.");
|
|
4452
5113
|
const granted = await chrome.permissions.request({ permissions: [permission] });
|
|
4453
5114
|
if (!granted) throw new Error("The capability grant was declined.");
|
|
5115
|
+
if (permission === "downloads") installmimelistener();
|
|
4454
5116
|
await audit("capability", `Capability ${permission} granted by the user.`);
|
|
4455
5117
|
return refreshcapabilities();
|
|
4456
5118
|
}
|
|
@@ -4524,13 +5186,27 @@ async function handlerequest(message, sender) {
|
|
|
4524
5186
|
for (const config of sheetendpoints) {
|
|
4525
5187
|
sheetgrants.push({ ...config, granted: await chrome.permissions.contains({ origins: [hostpattern(config.origin)] }).catch(() => false) });
|
|
4526
5188
|
}
|
|
5189
|
+
const downloads = await memory.getdownloads();
|
|
5190
|
+
const netlogs = await memory.getnetlog();
|
|
5191
|
+
const clipconsents = await memory.getclipconsents();
|
|
5192
|
+
const clips = await memory.getclips();
|
|
5193
|
+
const quarantines = await memory.getquarantines();
|
|
5194
|
+
const cleanuprules = await memory.getcleanuprules();
|
|
5195
|
+
const cleanupruns = await memory.getcleanupruns();
|
|
5196
|
+
const capturecounters = await memory.getcapturecounters();
|
|
5197
|
+
const inventory = await memory.getinventory();
|
|
5198
|
+
const mimefilters = await memory.getmimefilters();
|
|
5199
|
+
const scanhooks = [];
|
|
5200
|
+
for (const hook of await memory.getscanhooks()) {
|
|
5201
|
+
scanhooks.push({ ...hook, granted: await chrome.permissions.contains({ origins: [hostpattern(hook.origin)] }).catch(() => false) });
|
|
5202
|
+
}
|
|
4527
5203
|
const clones = clonetabs(tabs);
|
|
4528
5204
|
const taskgauge = tasktabgauge(tabs.filter((tab) => badges.some((badge) => badge.tabid === tab.tabid)).length, tasktabceiling(await memory.getsettings()));
|
|
4529
5205
|
const report = await buildtabreport(tabs);
|
|
4530
5206
|
const livetab = session ? await chrome.tabs.get(session.tabid).catch(() => void 0) : void 0;
|
|
4531
5207
|
const waitprofile = session ? waitprofiles.find((record2) => record2.origin === session.origin) : void 0;
|
|
4532
5208
|
const livestate = { phase: livetab?.status === "loading" ? "loading" : "complete", ...navrecords[0] ? { finalurl: navrecords[0].finalurl, redirects: navrecords[0].chain } : {} };
|
|
4533
|
-
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants };
|
|
5209
|
+
return { config: await memory.getconfig(), session, plan, progress: plan && progress?.planid === plan.id ? progress : void 0, diagnostic: await memory.getdiagnostic(), audit: await memory.getaudit(), capabilities: await refreshcapabilities(), outcomes: await memory.getoutcomes(), holds: heldkeysreport({ tabid: session?.tabid ?? 0, holds }), dialogs: await memory.getdialogs(), retries: await memory.getretries(), ...signals ? { signals: signalsreport({ signals }) } : { signals: signalsreport({}) }, banners: await memory.getbanners(), mutationevents: await memory.getmutationevents(), focusevents: await memory.getfocusevents(), diffs: await memory.getdiffs(), selectors: await memory.getselectors(), ...a11y ? { a11y } : {}, ...reader ? { reader } : {}, ...map ? { map } : {}, trail: trailreport({ ...session ? { sessionid: session.id } : {}, trail }), navrecords, ratestates, safeties, curated, waitprofiles, auths, navcontrol, navqueues, artifacts, navstate: livestate, ...waitprofile ? { waitprofile } : {}, offline: !navigator.onLine, tabs, windows, layouts: layoutreport({ layouts }), tabgroups, tabmetas, badges, snapshots, closedtabs, tabwatchevents, clones, tasktabgauge: taskgauge, ...controltab ? { controltab } : {}, tabreport: report, profiles, tickets, wizards: wizardreport({ ...session ? { sessionid: session.id } : {}, wizards, picks }), picks, errorreports, captchas, detections, ...codeentry !== void 0 ? { codeentry: true } : {}, datasets, imports, extractsessions, streams, exports, provenances, taskrules, sheetendpoints: sheetgrants, downloads, netlogs, clipconsents, clips, quarantines, cleanuprules, cleanupruns, capturecounters, inventory, mimefilters, scanhooks };
|
|
4534
5210
|
}
|
|
4535
5211
|
case "capabilities":
|
|
4536
5212
|
return refreshcapabilities();
|
|
@@ -4861,6 +5537,81 @@ async function handlerequest(message, sender) {
|
|
|
4861
5537
|
return extractionreportValue();
|
|
4862
5538
|
case "provenance":
|
|
4863
5539
|
return provenancereportValue();
|
|
5540
|
+
case "downloadreport": {
|
|
5541
|
+
const plan = await memory.getplan();
|
|
5542
|
+
if (!plan) throw new Error("No plan is available for a download report envelope.");
|
|
5543
|
+
return JSON.parse(downloadreport({ downloads: await memory.getdownloads(), plan }));
|
|
5544
|
+
}
|
|
5545
|
+
case "quarantine":
|
|
5546
|
+
return quarantinereport({ entries: await memory.getquarantines() });
|
|
5547
|
+
case "netlog":
|
|
5548
|
+
return netlogreport({ records: await memory.getnetlog() });
|
|
5549
|
+
case "downloadaction": {
|
|
5550
|
+
const inputdownload = message;
|
|
5551
|
+
const session = await memory.getsession();
|
|
5552
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Download actions stay behind the consent gate of an active session.");
|
|
5553
|
+
const record2 = await loadownload(inputdownload.id ?? "");
|
|
5554
|
+
if (!record2) throw new Error(`No stored download matches ${inputdownload.id ?? ""}.`);
|
|
5555
|
+
const extra = { sessionid: session.id };
|
|
5556
|
+
if (inputdownload.action === "pause") return pauseonerecord(record2, extra);
|
|
5557
|
+
if (inputdownload.action === "resume") return resumeonerecord(record2, extra);
|
|
5558
|
+
if (inputdownload.action === "verify") return verifyonerecord(record2, {}, extra);
|
|
5559
|
+
throw new Error("A pause, resume or verify action is required.");
|
|
5560
|
+
}
|
|
5561
|
+
case "approveclipconsent": {
|
|
5562
|
+
const inputconsent = message;
|
|
5563
|
+
const record2 = (await memory.getclipconsents()).find((item) => item.id === inputconsent.id);
|
|
5564
|
+
if (!record2) throw new Error("No clipboard consent prompt matches the requested id.");
|
|
5565
|
+
await memory.setclipconsent({ ...record2, approved: inputconsent.approved !== false });
|
|
5566
|
+
const session = await memory.getsession();
|
|
5567
|
+
await audit("clipboard", `Clipboard consent prompt ${record2.id} for step ${record2.stepid} on ${record2.origin} ${inputconsent.approved !== false ? "approved" : "declined"} by the user; every read consumes its own prompt.`, { ...session ? { sessionid: session.id } : {} });
|
|
5568
|
+
await refreshbadge();
|
|
5569
|
+
return { id: record2.id, approved: inputconsent.approved !== false };
|
|
5570
|
+
}
|
|
5571
|
+
case "releasequarantine": {
|
|
5572
|
+
const inputquarantine = message;
|
|
5573
|
+
const entry = (await memory.getquarantines()).find((item) => item.id === inputquarantine.id);
|
|
5574
|
+
if (!entry) throw new Error("No quarantined file matches the requested id.");
|
|
5575
|
+
if (entry.release !== void 0) throw new Error(`The quarantined file ${entry.path} was already released.`);
|
|
5576
|
+
const gate = quarantinereleasegranted(entry);
|
|
5577
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
5578
|
+
const releasedentry = released(entry, `user-${Date.now()}`, Date.now());
|
|
5579
|
+
await memory.setquarantine(releasedentry);
|
|
5580
|
+
const session = await memory.getsession();
|
|
5581
|
+
await audit("quarantine", `Quarantine release of ${entry.path} approved with the ${entry.scan} scan verdict under ref ${releasedentry.release}; every release is audited with its verdict.`, { ...session ? { sessionid: session.id } : {} });
|
|
5582
|
+
await refreshbadge();
|
|
5583
|
+
return releasedentry;
|
|
5584
|
+
}
|
|
5585
|
+
case "setcleanuprules": {
|
|
5586
|
+
const inputrules = message;
|
|
5587
|
+
const rules = (Array.isArray(inputrules.rules) ? inputrules.rules : []).filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
5588
|
+
for (const rule of rules) {
|
|
5589
|
+
const gate = validatecleanuprule(rule);
|
|
5590
|
+
if (!gate.allowed) throw new Error(gate.reason);
|
|
5591
|
+
}
|
|
5592
|
+
await memory.setcleanuprules(rules);
|
|
5593
|
+
const session = await memory.getsession();
|
|
5594
|
+
await audit("cleanup", `The review panel stored ${rules.length} reviewed cleanup rule${rules.length === 1 ? "" : "s"} with age windows and keep policies; ages stay user configured with no code ceiling.`, { ...session ? { sessionid: session.id } : {} });
|
|
5595
|
+
return { rules: rules.length };
|
|
5596
|
+
}
|
|
5597
|
+
case "configurescanhook": {
|
|
5598
|
+
const inputhook = message;
|
|
5599
|
+
const config = normalizeendpoint(inputhook.endpoint ?? "");
|
|
5600
|
+
const granted = await chrome.permissions.contains({ origins: [hostpattern(config.origin)] });
|
|
5601
|
+
if (!granted) throw new Error("The scan hook origin has not received optional permission.");
|
|
5602
|
+
if (!inputhook.scanner?.trim()) throw new Error("A scanner name is required for the scan hook.");
|
|
5603
|
+
const hook = { scanner: inputhook.scanner.trim(), endpoint: config.endpoint, origin: config.origin, configuredat: Date.now() };
|
|
5604
|
+
await memory.setscanhook(hook);
|
|
5605
|
+
await audit("quarantine", `Configured the virus scanning hook ${hook.scanner} at ${hook.origin}; scan verdicts arrive from the endpoint and hook failures stay pending verdicts.`, {});
|
|
5606
|
+
return hook;
|
|
5607
|
+
}
|
|
5608
|
+
case "exportnetlog": {
|
|
5609
|
+
const session = await memory.getsession();
|
|
5610
|
+
if (!session || session.stoppedat || session.expiresat <= Date.now()) throw new Error("Netlog exports stay behind the consent gate of an active session.");
|
|
5611
|
+
const records = (await memory.getnetlog()).map((record2) => ({ ...record2, headers: redactheaders(record2.headers ?? {}) }));
|
|
5612
|
+
await audit("observation", `The review panel exported ${records.length} netlog record${records.length === 1 ? "" : "s"} of the run with every header value redacted.`, { sessionid: session.id });
|
|
5613
|
+
return { records, redacted: true, redaction: "every header value is redacted from exported netlogs" };
|
|
5614
|
+
}
|
|
4864
5615
|
case "stop": {
|
|
4865
5616
|
const session = await memory.getsession();
|
|
4866
5617
|
if (session) await memory.setsession({ ...session, stoppedat: Date.now() });
|