diff --git a/Assemblies/Scripts/Borealis/Remote_Agent_Update_WIN.json b/Assemblies/Scripts/Borealis/Remote_Agent_Update_WIN.json index 20f196e..689fd54 100644 --- a/Assemblies/Scripts/Borealis/Remote_Agent_Update_WIN.json +++ b/Assemblies/Scripts/Borealis/Remote_Agent_Update_WIN.json @@ -4,7 +4,7 @@ "description": "Reaches out to the remote Borealis agent and triggers an automatic unattended update from the Github repository.", "category": "script", "type": "powershell", - "script": "W0NtZGxldEJpbmRpbmcoKV0KcGFyYW0oCiAgICBbUGFyYW1ldGVyKCldCiAgICBbc3RyaW5nXSRUYXNrTmFtZSA9ICJCb3JlYWxpcyBBZ2VudCIsCgogICAgW1BhcmFtZXRlcigpXQogICAgW3N0cmluZ10kVGFza1BhdGgKKQoKIyAtLS0gRW52aXJvbm1lbnQtY29udHJvbGxlZCBtb2RlIC0tLQokdXBkYXRlTW9kZSA9ICgkZW52OnVwZGF0ZV9tb2RlKS5Ub0xvd2VySW52YXJpYW50KCkKaWYgKC1ub3QgJHVwZGF0ZU1vZGUpIHsgJHVwZGF0ZU1vZGUgPSAidXBkYXRlIiB9ICAjIGRlZmF1bHQgYmVoYXZpb3IKJGZvcmNlVXBkYXRlID0gJHVwZGF0ZU1vZGUgLWVxICJmb3JjZV91cGRhdGUiCgojIC0tLSBSZXBvc2l0b3J5IGluZm8gKGZpeGVkKSAtLS0KJFJlcG9Pd25lciA9ICJidW5ueS1sYWItaW8iCiRSZXBvTmFtZSAgPSAiQm9yZWFsaXMiCiRCcmFuY2ggICAgPSAibWFpbiIKCiMgcmVnaW9uOiBoZWxwZXIgLSBmZXRjaCBsYXRlc3QgU0hBIGZyb20gR2l0SHViIEFQSSAobm8gRVRhZykKZnVuY3Rpb24gR2V0LUdpdEh1YkJyYW5jaFNoYSB7CiAgICBwYXJhbSgKICAgICAgICBbc3RyaW5nXSRPd25lciwKICAgICAgICBbc3RyaW5nXSRSZXBvLAogICAgICAgIFtzdHJpbmddJEJyYW5jaCwKICAgICAgICBbc3RyaW5nXSRUb2tlbgogICAgKQogICAgJHVyaSA9ICJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zLyRPd25lci8kUmVwby9icmFuY2hlcy8kQnJhbmNoIgogICAgJGhlYWRlcnMgPSBAewogICAgICAgICJVc2VyLUFnZW50IiA9ICJib3JlYWxpcy1hZ2VudC11cGRhdGVyIgogICAgICAgICJBY2NlcHQiICAgICA9ICJhcHBsaWNhdGlvbi92bmQuZ2l0aHViK2pzb24iCiAgICB9CiAgICBpZiAoJFRva2VuKSB7ICRoZWFkZXJzWyJBdXRob3JpemF0aW9uIl0gPSAiQmVhcmVyICRUb2tlbiIgfQoKICAgICRyZXNwID0gSW52b2tlLVdlYlJlcXVlc3QgLVVyaSAkdXJpIC1NZXRob2QgR0VUIC1IZWFkZXJzICRoZWFkZXJzIC1Vc2VCYXNpY1BhcnNpbmcgLUVycm9yQWN0aW9uIFN0b3AKICAgICRqc29uID0gJHJlc3AuQ29udGVudCB8IENvbnZlcnRGcm9tLUpzb24KICAgIHJldHVybiAkanNvbi5jb21taXQuc2hhCn0KIyBlbmRyZWdpb24KCiMgLS0tIExvY2F0ZSBBZ2VudCBmb2xkZXIgdmlhIHNjaGVkdWxlZCB0YXNrIC0tLQokdGFza1BhcmFtcyA9IEB7IFRhc2tOYW1lID0gJFRhc2tOYW1lOyBFcnJvckFjdGlvbiA9ICdTdG9wJyB9CmlmICgkVGFza1BhdGgpIHsgJHRhc2tQYXJhbXMuVGFza1BhdGggPSAkVGFza1BhdGggfQoKdHJ5IHsKICAgICR0YXNrID0gR2V0LVNjaGVkdWxlZFRhc2sgQHRhc2tQYXJhbXMKfSBjYXRjaCB7CiAgICB0aHJvdyAiU2NoZWR1bGVkIHRhc2sgJyRUYXNrTmFtZScgd2FzIG5vdCBmb3VuZC4iCn0KCiRleGVjQWN0aW9uID0gJHRhc2suQWN0aW9ucyB8IFdoZXJlLU9iamVjdCB7ICRfLkNpbUNsYXNzLkNpbUNsYXNzTmFtZSAtZXEgJ01TRlRfVGFza0V4ZWNBY3Rpb24nIH0gfCBTZWxlY3QtT2JqZWN0IC1GaXJzdCAxCmlmICgtbm90ICRleGVjQWN0aW9uKSB7IHRocm93ICJTY2hlZHVsZWQgdGFzayAnJFRhc2tOYW1lJyBkb2VzIG5vdCBjb250YWluIGFuIGV4ZWN1dGFibGUgYWN0aW9uLiIgfQoKJHdvcmtpbmdEaXJlY3RvcnkgPSAkZXhlY0FjdGlvbi5Xb3JraW5nRGlyZWN0b3J5CmlmIChbc3RyaW5nXTo6SXNOdWxsT3JXaGl0ZVNwYWNlKCR3b3JraW5nRGlyZWN0b3J5KSkgewogICAgJGNhbmRpZGF0ZSA9IFNwbGl0LVBhdGggLVBhdGggJGV4ZWNBY3Rpb24uRXhlY3V0ZSAtUGFyZW50CiAgICBpZiAoW3N0cmluZ106OklzTnVsbE9yV2hpdGVTcGFjZSgkY2FuZGlkYXRlKSkgewogICAgICAgIHRocm93ICJVbmFibGUgdG8gZGV0ZXJtaW5lIHdvcmtpbmcgZGlyZWN0b3J5IGZvciAnJFRhc2tOYW1lJy4iCiAgICB9CiAgICAkd29ya2luZ0RpcmVjdG9yeSA9ICRjYW5kaWRhdGUKfQoKdHJ5IHsKICAgICRhZ2VudFJvb3QgPSBSZXNvbHZlLVBhdGggLVBhdGggJHdvcmtpbmdEaXJlY3RvcnkgLUVycm9yQWN0aW9uIFN0b3AKfSBjYXRjaCB7CiAgICB0aHJvdyAiVGhlIHdvcmtpbmcgZGlyZWN0b3J5ICckd29ya2luZ0RpcmVjdG9yeScgZG9lcyBub3QgZXhpc3QuIgp9Cgp0cnkgewogICAgJHJlcG9Sb290ID0gUmVzb2x2ZS1QYXRoIC1QYXRoIChKb2luLVBhdGggJGFnZW50Um9vdCAnLi5cLi4nKSAtRXJyb3JBY3Rpb24gU3RvcAp9IGNhdGNoIHsKICAgIHRocm93ICJVbmFibGUgdG8gcmVzb2x2ZSBCb3JlYWxpcyByZXBvc2l0b3J5IHJvb3QgZnJvbSAnJGFnZW50Um9vdCcuIgp9CgokdXBkYXRlU2NyaXB0ID0gSm9pbi1QYXRoICRyZXBvUm9vdCAnQm9yZWFsaXMucHMxJwppZiAoLW5vdCAoVGVzdC1QYXRoICR1cGRhdGVTY3JpcHQgLVBhdGhUeXBlIExlYWYpKSB7CiAgICB0aHJvdyAiQm9yZWFsaXMucHMxIG5vdCBmb3VuZCBhdCAnJHVwZGF0ZVNjcmlwdCcuIgp9CgpXcml0ZS1WZXJib3NlICJBZ2VudCByb290OiAkYWdlbnRSb290IgpXcml0ZS1WZXJib3NlICJSZXBvIHJvb3Q6ICRyZXBvUm9vdCIKCiMgLS0tIFNpbmdsZS1maWxlIFNIQSBzdGF0ZSBhdCBhZ2VudCByb290IC0tLQokaGFzaEZpbGUgPSBKb2luLVBhdGggJGFnZW50Um9vdCAnZ2l0aHViX3JlcG9faGFzaC50eHQnCgojIC0tLSBPcHRpb25hbCBHaXRIdWIgdG9rZW4gZm9yIHByaXZhdGUgYWNjZXNzIG9yIGhpZ2hlciBsaW1pdHMgLS0tCiR0b2tlbiA9ICRlbnY6R0lUSFVCX1RPS0VOCgojIC0tLSBMb2FkIGxhc3Qta25vd24gU0hBIChQUyA1LjEgc2FmZSkgLS0tCiRsYXN0U2hhID0gJG51bGwKaWYgKFRlc3QtUGF0aCAkaGFzaEZpbGUpIHsKICAgICRsYXN0U2hhID0gKEdldC1Db250ZW50ICRoYXNoRmlsZSAtUmF3KS5UcmltKCkKfQoKIyAtLS0gUXVlcnkgR2l0SHViIGZvciBjdXJyZW50IFNIQSAobm8gRVRhZykgLS0tCiRuZXdTaGEgPSAkbnVsbAp0cnkgewogICAgJG5ld1NoYSA9IEdldC1HaXRIdWJCcmFuY2hTaGEgLU93bmVyICRSZXBvT3duZXIgLVJlcG8gJFJlcG9OYW1lIC1CcmFuY2ggJEJyYW5jaCAtVG9rZW4gJHRva2VuCn0gY2F0Y2ggewogICAgdGhyb3cgIkZhaWxlZCB0byBxdWVyeSBHaXRIdWIgZm9yIGxhdGVzdCBTSEE6ICQoJF8uRXhjZXB0aW9uLk1lc3NhZ2UpIgp9CgojIC0tLSBEZXRlcm1pbmUgY2hhbmdlIC0tLQokY2hhbmdlRGV0ZWN0ZWQgPSAkZmFsc2UKaWYgKC1ub3QgJGxhc3RTaGEpIHsKICAgIFdyaXRlLVZlcmJvc2UgIk5vIHByaW9yIFNIQSBvbiBkaXNrOyB0cmVhdGluZyBhcyBmaXJzdCBydW4uIgogICAgJGNoYW5nZURldGVjdGVkID0gJHRydWUKfSBlbHNlaWYgKCRsYXN0U2hhIC1uZSAkbmV3U2hhKSB7CiAgICAkY2hhbmdlRGV0ZWN0ZWQgPSAkdHJ1ZQogICAgV3JpdGUtVmVyYm9zZSAiUmVwbyB1cGRhdGVkOiAkbGFzdFNoYSAtPiAkbmV3U2hhIgp9IGVsc2UgewogICAgV3JpdGUtVmVyYm9zZSAiU0hBIHVuY2hhbmdlZDogJG5ld1NoYSIKfQoKIyAtLS0gRGVjaWRlIGlmIHVwZGF0ZSBzaG91bGQgcnVuIC0tLQokc2hvdWxkVXBkYXRlID0gJGZhbHNlCmlmICgkZm9yY2VVcGRhdGUpIHsKICAgIFdyaXRlLVZlcmJvc2UgInVwZGF0ZV9tb2RlPWZvcmNlX3VwZGF0ZSDihpIgZm9yY2luZyB1cGRhdGUuIgogICAgJHNob3VsZFVwZGF0ZSA9ICR0cnVlCn0gZWxzZWlmICgkY2hhbmdlRGV0ZWN0ZWQpIHsKICAgIFdyaXRlLVZlcmJvc2UgInVwZGF0ZV9tb2RlPXVwZGF0ZSDihpIgcmVwbyBjaGFuZ2VkIG9yIGZpcnN0IHJ1bi4iCiAgICAkc2hvdWxkVXBkYXRlID0gJHRydWUKfSBlbHNlIHsKICAgIFdyaXRlLVZlcmJvc2UgIk5vIGNoYW5nZTsgc2tpcHBpbmcgQm9yZWFsaXMucHMxIC1TaWxlbnRVcGRhdGUuIgp9CgppZiAoLW5vdCAkc2hvdWxkVXBkYXRlKSB7CiAgICBXcml0ZS1Ib3N0ICI9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09IgogICAgV3JpdGUtSG9zdCAiQm9yZWFsaXMgQWdlbnQgQWxyZWFkeSBVcC10by1EYXRlIgogICAgV3JpdGUtSG9zdCAiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIKICAgIHJldHVybgp9CgojIC0tLSBTSU5HTEVUT04gTVVURVg6IHByZXZlbnQgY29uY3VycmVudCB1cGRhdGVycyBmcm9tIHN0b21waW5nIHRoZSBzYW1lIHppcCAtLS0KJG11dGV4ID0gJG51bGwKJGdvdE11dGV4ID0gJGZhbHNlCnRyeSB7CiAgICAkbXV0ZXggPSBOZXctT2JqZWN0IFN5c3RlbS5UaHJlYWRpbmcuTXV0ZXgoJGZhbHNlLCAiR2xvYmFsXEJvcmVhbGlzVXBkYXRlIikKICAgICRnb3RNdXRleCA9ICRtdXRleC5XYWl0T25lKDApCiAgICBpZiAoLW5vdCAkZ290TXV0ZXgpIHsKICAgICAgICBXcml0ZS1WZXJib3NlICJBbm90aGVyIHVwZGF0ZSBpcyBhbHJlYWR5IHJ1bm5pbmcgKG11dGV4IGhlbGQpLiBFeGl0aW5nIHF1aWV0bHkuIgogICAgICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCiAgICAgICAgV3JpdGUtSG9zdCAiQm9yZWFsaXMgQWdlbnQgQWxyZWFkeSBVcC10by1EYXRlIgogICAgICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCiAgICAgICAgcmV0dXJuCiAgICB9CgogICAgIyAtLS0gUFJFLUZMSUdIVDogcHVyZ2Ugc3RhbGUgJ21haW4uemlwJyB3aXRoIHJldHJpZXMgdG8gYXZvaWQgJ2luIHVzZScgZXJyb3JzIC0tLQogICAgJHN0YWdpbmcgPSBKb2luLVBhdGggJHJlcG9Sb290ICdVcGRhdGVfU3RhZ2luZycKICAgICR6aXBQYXRoID0gSm9pbi1QYXRoICRzdGFnaW5nICdtYWluLnppcCcKICAgIGlmIChUZXN0LVBhdGggJHppcFBhdGgpIHsKICAgICAgICBXcml0ZS1WZXJib3NlICJQcmUtZmxpZ2h0OiByZW1vdmluZyBleGlzdGluZyAkemlwUGF0aCIKICAgICAgICBmb3IgKCRpPTE7ICRpIC1sZSAxMDsgJGkrKykgewogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgUmVtb3ZlLUl0ZW0gLUxpdGVyYWxQYXRoICR6aXBQYXRoIC1Gb3JjZSAtRXJyb3JBY3Rpb24gU3RvcAogICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgfSBjYXRjaCB7CiAgICAgICAgICAgICAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzICgxMDAgKiAkaSkgICMgYmFja29mZjogMTAwLi4xMDAwbXMKICAgICAgICAgICAgICAgIGlmICgkaSAtZXEgMTApIHsgdGhyb3cgIlByZS1mbGlnaHQgZGVsZXRlIGZhaWxlZDsgJHppcFBhdGggYXBwZWFycyBsb2NrZWQgYnkgYW5vdGhlciBwcm9jZXNzLiIgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQoKICAgICMgT3B0aW9uYWw6IGdpdmUgY2hpbGQgc2NyaXB0IHRoZSBTSEE7IEJvcmVhbGlzLnBzMSBjYW4gY2hvb3NlIHRvIG5hbWUgdW5pcXVlIGFyY2hpdmVzIGlmIGRlc2lyZWQKICAgICRlbnY6Qk9SRUFMSVNfRVhQRUNURURfU0hBID0gJG5ld1NoYQoKICAgICMgLS0tIFBlcmZvcm0gdXBkYXRlIC0tLQogICAgV3JpdGUtVmVyYm9zZSAiSW52b2tpbmcgQm9yZWFsaXMucHMxIC1TaWxlbnRVcGRhdGUuLi4iCiAgICBQdXNoLUxvY2F0aW9uICRyZXBvUm9vdAogICAgJHVwZGF0ZVN1Y2NlZWRlZCA9ICRmYWxzZQogICAgdHJ5IHsKICAgICAgICAmICR1cGRhdGVTY3JpcHQgLVNpbGVudFVwZGF0ZQogICAgICAgICR1cGRhdGVTdWNjZWVkZWQgPSAkPwogICAgfSBmaW5hbGx5IHsKICAgICAgICBQb3AtTG9jYXRpb24KICAgIH0KCiAgICBpZiAoLW5vdCAkdXBkYXRlU3VjY2VlZGVkKSB7CiAgICAgICAgdGhyb3cgIkJvcmVhbGlzLnBzMSAtU2lsZW50VXBkYXRlIGZhaWxlZDsgbm90IGFkdmFuY2luZyBzdG9yZWQgU0hBLiIKICAgIH0KCiAgICAjIC0tLSBTYXZlIG5ldyBTSEEgb25seSBhZnRlciBzdWNjZXNzIC0tLQogICAgaWYgKCRuZXdTaGEpIHsKICAgICAgICBTZXQtQ29udGVudCAtUGF0aCAkaGFzaEZpbGUgLVZhbHVlICRuZXdTaGEgLUVuY29kaW5nIEFTQ0lJCiAgICB9CgogICAgV3JpdGUtSG9zdCAiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIKICAgIFdyaXRlLUhvc3QgKCJCb3JlYWxpcyBBZ2VudCBVcGRhdGVkIC0gTmV3IEdpdGh1YiBSZXBvc2l0b3J5IEhhc2g6IHswfSIgLWYgJG5ld1NoYSkKICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCn0KZmluYWxseSB7CiAgICBpZiAoJG11dGV4IC1hbmQgJGdvdE11dGV4KSB7ICRtdXRleC5SZWxlYXNlTXV0ZXgoKSB8IE91dC1OdWxsIH0KICAgIGlmICgkbXV0ZXgpIHsgJG11dGV4LkRpc3Bvc2UoKSB9Cn0K", + "script": "W0NtZGxldEJpbmRpbmcoKV0KcGFyYW0oCiAgICBbUGFyYW1ldGVyKCldCiAgICBbc3RyaW5nXSRUYXNrTmFtZSA9ICJCb3JlYWxpcyBBZ2VudCIsCgogICAgW1BhcmFtZXRlcigpXQogICAgW3N0cmluZ10kVGFza1BhdGgKKQoKIyAtLS0gRW52aXJvbm1lbnQtY29udHJvbGxlZCBtb2RlIC0tLQokdXBkYXRlTW9kZSA9ICgkZW52OnVwZGF0ZV9tb2RlKS5Ub0xvd2VySW52YXJpYW50KCkKaWYgKC1ub3QgJHVwZGF0ZU1vZGUpIHsgJHVwZGF0ZU1vZGUgPSAidXBkYXRlIiB9ICAjIGRlZmF1bHQgYmVoYXZpb3IKJGZvcmNlVXBkYXRlID0gJHVwZGF0ZU1vZGUgLWVxICJmb3JjZV91cGRhdGUiCgojIC0tLSBSZXBvc2l0b3J5IGluZm8gKGZpeGVkKSAtLS0KJFJlcG9Pd25lciA9ICJidW5ueS1sYWItaW8iCiRSZXBvTmFtZSAgPSAiQm9yZWFsaXMiCiRCcmFuY2ggICAgPSAibWFpbiIKCiMgcmVnaW9uOiBoZWxwZXIgLSBmZXRjaCBjYWNoZWQgcmVwb3NpdG9yeSBTSEEgZnJvbSBCb3JlYWxpcyBzZXJ2ZXIKZnVuY3Rpb24gR2V0LUJvcmVhbGlzU2VydmVyUmVwb1NoYSB7CiAgICBwYXJhbSgKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSA9ICR0cnVlKV0KICAgICAgICBbc3RyaW5nXSRCYXNlVXJsLAoKICAgICAgICBbUGFyYW1ldGVyKE1hbmRhdG9yeSA9ICR0cnVlKV0KICAgICAgICBbc3RyaW5nXSRPd25lciwKCiAgICAgICAgW1BhcmFtZXRlcihNYW5kYXRvcnkgPSAkdHJ1ZSldCiAgICAgICAgW3N0cmluZ10kUmVwbywKCiAgICAgICAgW1BhcmFtZXRlcihNYW5kYXRvcnkgPSAkdHJ1ZSldCiAgICAgICAgW3N0cmluZ10kQnJhbmNoCiAgICApCgogICAgaWYgKFtzdHJpbmddOjpJc051bGxPcldoaXRlU3BhY2UoJEJhc2VVcmwpKSB7CiAgICAgICAgdGhyb3cgIlNlcnZlciBVUkwgaXMgYmxhbms7IGNhbm5vdCBxdWVyeSByZXBvIGhhc2guIgogICAgfQoKICAgICRiYXNlID0gJEJhc2VVcmwuVHJpbUVuZCgnLycpCiAgICAkcmVwb1BhcmFtID0gW1N5c3RlbS5VcmldOjpFc2NhcGVEYXRhU3RyaW5nKCIkT3duZXIvJFJlcG8iKQogICAgJGJyYW5jaFBhcmFtID0gW1N5c3RlbS5VcmldOjpFc2NhcGVEYXRhU3RyaW5nKCRCcmFuY2gpCiAgICAkdXJpID0gIiRiYXNlL2FwaS9hZ2VudC9yZXBvX2hhc2g/cmVwbz0kcmVwb1BhcmFtJmJyYW5jaD0kYnJhbmNoUGFyYW0iCgogICAgJGhlYWRlcnMgPSBAeyAiVXNlci1BZ2VudCIgPSAiYm9yZWFsaXMtYWdlbnQtdXBkYXRlciIgfQoKICAgICRyZXNwID0gSW52b2tlLVdlYlJlcXVlc3QgLVVyaSAkdXJpIC1NZXRob2QgR0VUIC1IZWFkZXJzICRoZWFkZXJzIC1Vc2VCYXNpY1BhcnNpbmcgLUVycm9yQWN0aW9uIFN0b3AKICAgICRqc29uID0gJHJlc3AuQ29udGVudCB8IENvbnZlcnRGcm9tLUpzb24KCiAgICBpZiAoJHJlc3AuU3RhdHVzQ29kZSAtbmUgMjAwKSB7CiAgICAgICAgJG1lc3NhZ2UgPSAkanNvbi5lcnJvcgogICAgICAgIGlmICgtbm90ICRtZXNzYWdlKSB7ICRtZXNzYWdlID0gIkhUVFAgJCgkcmVzcC5TdGF0dXNDb2RlKSIgfQogICAgICAgIHRocm93ICJCb3JlYWxpcyBzZXJ2ZXIgcmVzcG9uZGVkIHdpdGggYW4gZXJyb3I6ICRtZXNzYWdlIgogICAgfQoKICAgICRzaGEgPSAoJGpzb24uc2hhKQogICAgaWYgKC1ub3QgJHNoYSkgewogICAgICAgICRtZXNzYWdlID0gJGpzb24uZXJyb3IKICAgICAgICBpZiAoJG1lc3NhZ2UpIHsKICAgICAgICAgICAgdGhyb3cgIkJvcmVhbGlzIHNlcnZlciBkaWQgbm90IHJldHVybiBhIHJlcG9zaXRvcnkgaGFzaDogJG1lc3NhZ2UiCiAgICAgICAgfQogICAgICAgIHRocm93ICJCb3JlYWxpcyBzZXJ2ZXIgZGlkIG5vdCByZXR1cm4gYSByZXBvc2l0b3J5IGhhc2guIgogICAgfQoKICAgIHJldHVybiAoJHNoYS5Ub1N0cmluZygpKS5UcmltKCkKfQojIGVuZHJlZ2lvbgoKIyAtLS0gTG9jYXRlIEFnZW50IGZvbGRlciB2aWEgc2NoZWR1bGVkIHRhc2sgLS0tCiR0YXNrUGFyYW1zID0gQHsgVGFza05hbWUgPSAkVGFza05hbWU7IEVycm9yQWN0aW9uID0gJ1N0b3AnIH0KaWYgKCRUYXNrUGF0aCkgeyAkdGFza1BhcmFtcy5UYXNrUGF0aCA9ICRUYXNrUGF0aCB9Cgp0cnkgewogICAgJHRhc2sgPSBHZXQtU2NoZWR1bGVkVGFzayBAdGFza1BhcmFtcwp9IGNhdGNoIHsKICAgIHRocm93ICJTY2hlZHVsZWQgdGFzayAnJFRhc2tOYW1lJyB3YXMgbm90IGZvdW5kLiIKfQoKJGV4ZWNBY3Rpb24gPSAkdGFzay5BY3Rpb25zIHwgV2hlcmUtT2JqZWN0IHsgJF8uQ2ltQ2xhc3MuQ2ltQ2xhc3NOYW1lIC1lcSAnTVNGVF9UYXNrRXhlY0FjdGlvbicgfSB8IFNlbGVjdC1PYmplY3QgLUZpcnN0IDEKaWYgKC1ub3QgJGV4ZWNBY3Rpb24pIHsgdGhyb3cgIlNjaGVkdWxlZCB0YXNrICckVGFza05hbWUnIGRvZXMgbm90IGNvbnRhaW4gYW4gZXhlY3V0YWJsZSBhY3Rpb24uIiB9Cgokd29ya2luZ0RpcmVjdG9yeSA9ICRleGVjQWN0aW9uLldvcmtpbmdEaXJlY3RvcnkKaWYgKFtzdHJpbmddOjpJc051bGxPcldoaXRlU3BhY2UoJHdvcmtpbmdEaXJlY3RvcnkpKSB7CiAgICAkY2FuZGlkYXRlID0gU3BsaXQtUGF0aCAtUGF0aCAkZXhlY0FjdGlvbi5FeGVjdXRlIC1QYXJlbnQKICAgIGlmIChbc3RyaW5nXTo6SXNOdWxsT3JXaGl0ZVNwYWNlKCRjYW5kaWRhdGUpKSB7CiAgICAgICAgdGhyb3cgIlVuYWJsZSB0byBkZXRlcm1pbmUgd29ya2luZyBkaXJlY3RvcnkgZm9yICckVGFza05hbWUnLiIKICAgIH0KICAgICR3b3JraW5nRGlyZWN0b3J5ID0gJGNhbmRpZGF0ZQp9Cgp0cnkgewogICAgJGFnZW50Um9vdCA9IFJlc29sdmUtUGF0aCAtUGF0aCAkd29ya2luZ0RpcmVjdG9yeSAtRXJyb3JBY3Rpb24gU3RvcAp9IGNhdGNoIHsKICAgIHRocm93ICJUaGUgd29ya2luZyBkaXJlY3RvcnkgJyR3b3JraW5nRGlyZWN0b3J5JyBkb2VzIG5vdCBleGlzdC4iCn0KCnRyeSB7CiAgICAkcmVwb1Jvb3QgPSBSZXNvbHZlLVBhdGggLVBhdGggKEpvaW4tUGF0aCAkYWdlbnRSb290ICcuLlwuLicpIC1FcnJvckFjdGlvbiBTdG9wCn0gY2F0Y2ggewogICAgdGhyb3cgIlVuYWJsZSB0byByZXNvbHZlIEJvcmVhbGlzIHJlcG9zaXRvcnkgcm9vdCBmcm9tICckYWdlbnRSb290Jy4iCn0KCiR1cGRhdGVTY3JpcHQgPSBKb2luLVBhdGggJHJlcG9Sb290ICdCb3JlYWxpcy5wczEnCmlmICgtbm90IChUZXN0LVBhdGggJHVwZGF0ZVNjcmlwdCAtUGF0aFR5cGUgTGVhZikpIHsKICAgIHRocm93ICJCb3JlYWxpcy5wczEgbm90IGZvdW5kIGF0ICckdXBkYXRlU2NyaXB0Jy4iCn0KCldyaXRlLVZlcmJvc2UgIkFnZW50IHJvb3Q6ICRhZ2VudFJvb3QiCldyaXRlLVZlcmJvc2UgIlJlcG8gcm9vdDogJHJlcG9Sb290IgoKIyAtLS0gRGV0ZXJtaW5lIEJvcmVhbGlzIHNlcnZlciBVUkwgLS0tCiRzZXJ2ZXJCYXNlVXJsID0gJGVudjpCT1JFQUxJU19TRVJWRVJfVVJMCmlmICgtbm90ICRzZXJ2ZXJCYXNlVXJsKSB7CiAgICAkc2V0dGluZ3NEaXIgPSBKb2luLVBhdGggJGFnZW50Um9vdCAnU2V0dGluZ3MnCiAgICAkc2VydmVyVXJsRmlsZSA9IEpvaW4tUGF0aCAkc2V0dGluZ3NEaXIgJ3NlcnZlcl91cmwudHh0JwogICAgaWYgKFRlc3QtUGF0aCAkc2VydmVyVXJsRmlsZSkgewogICAgICAgICRzZXJ2ZXJCYXNlVXJsID0gKEdldC1Db250ZW50ICRzZXJ2ZXJVcmxGaWxlIC1SYXcpLlRyaW0oKQogICAgfQp9CmlmICgtbm90ICRzZXJ2ZXJCYXNlVXJsKSB7ICRzZXJ2ZXJCYXNlVXJsID0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NTAwMCcgfQokc2VydmVyQmFzZVVybCA9ICRzZXJ2ZXJCYXNlVXJsLlRyaW0oKQpXcml0ZS1WZXJib3NlICJVc2luZyBCb3JlYWxpcyBzZXJ2ZXIgVVJMOiAkc2VydmVyQmFzZVVybCIKCiMgLS0tIFNpbmdsZS1maWxlIFNIQSBzdGF0ZSBhdCBhZ2VudCByb290IC0tLQokaGFzaEZpbGUgPSBKb2luLVBhdGggJGFnZW50Um9vdCAnZ2l0aHViX3JlcG9faGFzaC50eHQnCgojIC0tLSBMb2FkIGxhc3Qta25vd24gU0hBIChQUyA1LjEgc2FmZSkgLS0tCiRsYXN0U2hhID0gJG51bGwKaWYgKFRlc3QtUGF0aCAkaGFzaEZpbGUpIHsKICAgICRsYXN0U2hhID0gKEdldC1Db250ZW50ICRoYXNoRmlsZSAtUmF3KS5UcmltKCkKfQoKIyAtLS0gUXVlcnkgQm9yZWFsaXMgc2VydmVyIGZvciBjdXJyZW50IFNIQSAtLS0KJG5ld1NoYSA9ICRudWxsCnRyeSB7CiAgICAkbmV3U2hhID0gR2V0LUJvcmVhbGlzU2VydmVyUmVwb1NoYSAtQmFzZVVybCAkc2VydmVyQmFzZVVybCAtT3duZXIgJFJlcG9Pd25lciAtUmVwbyAkUmVwb05hbWUgLUJyYW5jaCAkQnJhbmNoCn0gY2F0Y2ggewogICAgdGhyb3cgIkZhaWxlZCB0byBxdWVyeSBCb3JlYWxpcyBzZXJ2ZXIgZm9yIGxhdGVzdCBTSEE6ICQoJF8uRXhjZXB0aW9uLk1lc3NhZ2UpIgp9CgojIC0tLSBEZXRlcm1pbmUgY2hhbmdlIC0tLQokY2hhbmdlRGV0ZWN0ZWQgPSAkZmFsc2UKaWYgKC1ub3QgJGxhc3RTaGEpIHsKICAgIFdyaXRlLVZlcmJvc2UgIk5vIHByaW9yIFNIQSBvbiBkaXNrOyB0cmVhdGluZyBhcyBmaXJzdCBydW4uIgogICAgJGNoYW5nZURldGVjdGVkID0gJHRydWUKfSBlbHNlaWYgKCRsYXN0U2hhIC1uZSAkbmV3U2hhKSB7CiAgICAkY2hhbmdlRGV0ZWN0ZWQgPSAkdHJ1ZQogICAgV3JpdGUtVmVyYm9zZSAiUmVwbyB1cGRhdGVkOiAkbGFzdFNoYSAtPiAkbmV3U2hhIgp9IGVsc2UgewogICAgV3JpdGUtVmVyYm9zZSAiU0hBIHVuY2hhbmdlZDogJG5ld1NoYSIKfQoKIyAtLS0gRGVjaWRlIGlmIHVwZGF0ZSBzaG91bGQgcnVuIC0tLQokc2hvdWxkVXBkYXRlID0gJGZhbHNlCmlmICgkZm9yY2VVcGRhdGUpIHsKICAgIFdyaXRlLVZlcmJvc2UgInVwZGF0ZV9tb2RlPWZvcmNlX3VwZGF0ZSDihpIgZm9yY2luZyB1cGRhdGUuIgogICAgJHNob3VsZFVwZGF0ZSA9ICR0cnVlCn0gZWxzZWlmICgkY2hhbmdlRGV0ZWN0ZWQpIHsKICAgIFdyaXRlLVZlcmJvc2UgInVwZGF0ZV9tb2RlPXVwZGF0ZSDihpIgcmVwbyBjaGFuZ2VkIG9yIGZpcnN0IHJ1bi4iCiAgICAkc2hvdWxkVXBkYXRlID0gJHRydWUKfSBlbHNlIHsKICAgIFdyaXRlLVZlcmJvc2UgIk5vIGNoYW5nZTsgc2tpcHBpbmcgQm9yZWFsaXMucHMxIC1TaWxlbnRVcGRhdGUuIgp9CgppZiAoLW5vdCAkc2hvdWxkVXBkYXRlKSB7CiAgICBXcml0ZS1Ib3N0ICI9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09IgogICAgV3JpdGUtSG9zdCAiQm9yZWFsaXMgQWdlbnQgQWxyZWFkeSBVcC10by1EYXRlIgogICAgV3JpdGUtSG9zdCAiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIKICAgIHJldHVybgp9CgojIC0tLSBTSU5HTEVUT04gTVVURVg6IHByZXZlbnQgY29uY3VycmVudCB1cGRhdGVycyBmcm9tIHN0b21waW5nIHRoZSBzYW1lIHppcCAtLS0KJG11dGV4ID0gJG51bGwKJGdvdE11dGV4ID0gJGZhbHNlCnRyeSB7CiAgICAkbXV0ZXggPSBOZXctT2JqZWN0IFN5c3RlbS5UaHJlYWRpbmcuTXV0ZXgoJGZhbHNlLCAiR2xvYmFsXEJvcmVhbGlzVXBkYXRlIikKICAgICRnb3RNdXRleCA9ICRtdXRleC5XYWl0T25lKDApCiAgICBpZiAoLW5vdCAkZ290TXV0ZXgpIHsKICAgICAgICBXcml0ZS1WZXJib3NlICJBbm90aGVyIHVwZGF0ZSBpcyBhbHJlYWR5IHJ1bm5pbmcgKG11dGV4IGhlbGQpLiBFeGl0aW5nIHF1aWV0bHkuIgogICAgICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCiAgICAgICAgV3JpdGUtSG9zdCAiQm9yZWFsaXMgQWdlbnQgQWxyZWFkeSBVcC10by1EYXRlIgogICAgICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCiAgICAgICAgcmV0dXJuCiAgICB9CgogICAgIyAtLS0gUFJFLUZMSUdIVDogcHVyZ2Ugc3RhbGUgJ21haW4uemlwJyB3aXRoIHJldHJpZXMgdG8gYXZvaWQgJ2luIHVzZScgZXJyb3JzIC0tLQogICAgJHN0YWdpbmcgPSBKb2luLVBhdGggJHJlcG9Sb290ICdVcGRhdGVfU3RhZ2luZycKICAgICR6aXBQYXRoID0gSm9pbi1QYXRoICRzdGFnaW5nICdtYWluLnppcCcKICAgIGlmIChUZXN0LVBhdGggJHppcFBhdGgpIHsKICAgICAgICBXcml0ZS1WZXJib3NlICJQcmUtZmxpZ2h0OiByZW1vdmluZyBleGlzdGluZyAkemlwUGF0aCIKICAgICAgICBmb3IgKCRpPTE7ICRpIC1sZSAxMDsgJGkrKykgewogICAgICAgICAgICB0cnkgewogICAgICAgICAgICAgICAgUmVtb3ZlLUl0ZW0gLUxpdGVyYWxQYXRoICR6aXBQYXRoIC1Gb3JjZSAtRXJyb3JBY3Rpb24gU3RvcAogICAgICAgICAgICAgICAgYnJlYWsKICAgICAgICAgICAgfSBjYXRjaCB7CiAgICAgICAgICAgICAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzICgxMDAgKiAkaSkgICMgYmFja29mZjogMTAwLi4xMDAwbXMKICAgICAgICAgICAgICAgIGlmICgkaSAtZXEgMTApIHsgdGhyb3cgIlByZS1mbGlnaHQgZGVsZXRlIGZhaWxlZDsgJHppcFBhdGggYXBwZWFycyBsb2NrZWQgYnkgYW5vdGhlciBwcm9jZXNzLiIgfQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfQoKICAgICMgT3B0aW9uYWw6IGdpdmUgY2hpbGQgc2NyaXB0IHRoZSBTSEE7IEJvcmVhbGlzLnBzMSBjYW4gY2hvb3NlIHRvIG5hbWUgdW5pcXVlIGFyY2hpdmVzIGlmIGRlc2lyZWQKICAgICRlbnY6Qk9SRUFMSVNfRVhQRUNURURfU0hBID0gJG5ld1NoYQoKICAgICMgLS0tIFBlcmZvcm0gdXBkYXRlIC0tLQogICAgV3JpdGUtVmVyYm9zZSAiSW52b2tpbmcgQm9yZWFsaXMucHMxIC1TaWxlbnRVcGRhdGUuLi4iCiAgICBQdXNoLUxvY2F0aW9uICRyZXBvUm9vdAogICAgJHVwZGF0ZVN1Y2NlZWRlZCA9ICRmYWxzZQogICAgdHJ5IHsKICAgICAgICAmICR1cGRhdGVTY3JpcHQgLVNpbGVudFVwZGF0ZQogICAgICAgICR1cGRhdGVTdWNjZWVkZWQgPSAkPwogICAgfSBmaW5hbGx5IHsKICAgICAgICBQb3AtTG9jYXRpb24KICAgIH0KCiAgICBpZiAoLW5vdCAkdXBkYXRlU3VjY2VlZGVkKSB7CiAgICAgICAgdGhyb3cgIkJvcmVhbGlzLnBzMSAtU2lsZW50VXBkYXRlIGZhaWxlZDsgbm90IGFkdmFuY2luZyBzdG9yZWQgU0hBLiIKICAgIH0KCiAgICAjIC0tLSBTYXZlIG5ldyBTSEEgb25seSBhZnRlciBzdWNjZXNzIC0tLQogICAgaWYgKCRuZXdTaGEpIHsKICAgICAgICBTZXQtQ29udGVudCAtUGF0aCAkaGFzaEZpbGUgLVZhbHVlICRuZXdTaGEgLUVuY29kaW5nIEFTQ0lJCiAgICB9CgogICAgV3JpdGUtSG9zdCAiPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSIKICAgIFdyaXRlLUhvc3QgKCJCb3JlYWxpcyBBZ2VudCBVcGRhdGVkIC0gTmV3IEdpdGh1YiBSZXBvc2l0b3J5IEhhc2g6IHswfSIgLWYgJG5ld1NoYSkKICAgIFdyaXRlLUhvc3QgIj09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0iCn0KZmluYWxseSB7CiAgICBpZiAoJG11dGV4IC1hbmQgJGdvdE11dGV4KSB7ICRtdXRleC5SZWxlYXNlTXV0ZXgoKSB8IE91dC1OdWxsIH0KICAgIGlmICgkbXV0ZXgpIHsgJG11dGV4LkRpc3Bvc2UoKSB9Cn0K", "timeout_seconds": 3600, "sites": { "mode": "all", @@ -22,4 +22,4 @@ ], "files": [], "script_encoding": "base64" -} \ No newline at end of file +} diff --git a/Data/Agent/Roles/role_DeviceAudit.py b/Data/Agent/Roles/role_DeviceAudit.py index b3b8a4b..0162fb6 100644 --- a/Data/Agent/Roles/role_DeviceAudit.py +++ b/Data/Agent/Roles/role_DeviceAudit.py @@ -9,6 +9,7 @@ import shutil import string import asyncio from pathlib import Path +from typing import Optional try: import psutil # type: ignore @@ -134,24 +135,132 @@ def _project_root(): return os.getcwd() -_AGENT_HASH_CACHE = {"path": None, "mtime": None, "value": None} +_AGENT_HASH_CACHE = { + "path": None, + "mtime": None, + "value": None, + "source": None, + "extra": None, +} + + +def _iter_hash_roots(): + seen = set() + root = _project_root() + for _ in range(6): + if not root or root in seen: + break + yield root + seen.add(root) + parent = os.path.dirname(root) + if not parent or parent == root: + break + root = parent + + +def _resolve_git_head_hash(root: str) -> Optional[str]: + git_dir = os.path.join(root, ".git") + head_path = os.path.join(git_dir, "HEAD") + if not os.path.isfile(head_path): + return None + try: + with open(head_path, "r", encoding="utf-8") as fh: + head = fh.read().strip() + except Exception: + return None + if not head: + return None + if head.startswith("ref:"): + ref = head.split(" ", 1)[1].strip() if " " in head else head.split(":", 1)[1].strip() + if not ref: + return None + ref_path = os.path.join(git_dir, *ref.split("/")) + if os.path.isfile(ref_path): + try: + with open(ref_path, "r", encoding="utf-8") as rf: + commit = rf.read().strip() + return commit or None + except Exception: + return None + packed_refs = os.path.join(git_dir, "packed-refs") + if os.path.isfile(packed_refs): + try: + with open(packed_refs, "r", encoding="utf-8") as pf: + for line in pf: + line = line.strip() + if not line or line.startswith("#") or line.startswith("^"): + continue + try: + commit, ref_name = line.split(" ", 1) + except ValueError: + continue + if ref_name.strip() == ref: + commit = commit.strip() + return commit or None + except Exception: + return None + return None + # Detached head contains the commit hash directly + commit = head.splitlines()[0].strip() + return commit or None def _read_agent_hash(): try: - root = _project_root() - path = os.path.join(root, 'github_repo_hash.txt') cache = _AGENT_HASH_CACHE - if not os.path.isfile(path): - cache.update({"path": path, "mtime": None, "value": None}) - return None - mtime = os.path.getmtime(path) - if cache.get("path") == path and cache.get("mtime") == mtime: + for root in _iter_hash_roots(): + path = os.path.join(root, 'github_repo_hash.txt') + if not os.path.isfile(path): + continue + mtime = os.path.getmtime(path) + if ( + cache.get("source") == "file" + and cache.get("path") == path + and cache.get("mtime") == mtime + ): + return cache.get("value") + with open(path, 'r', encoding='utf-8') as fh: + value = fh.read().strip() + cache.update( + { + "source": "file", + "path": path, + "mtime": mtime, + "extra": None, + "value": value or None, + } + ) return cache.get("value") - with open(path, 'r', encoding='utf-8') as fh: - value = fh.read().strip() - cache.update({"path": path, "mtime": mtime, "value": value or None}) - return cache.get("value") + + for root in _iter_hash_roots(): + git_dir = os.path.join(root, '.git') + head_path = os.path.join(git_dir, 'HEAD') + if not os.path.isfile(head_path): + continue + head_mtime = os.path.getmtime(head_path) + packed_path = os.path.join(git_dir, 'packed-refs') + packed_mtime = os.path.getmtime(packed_path) if os.path.isfile(packed_path) else None + if ( + cache.get("source") == "git" + and cache.get("path") == head_path + and cache.get("mtime") == head_mtime + and cache.get("extra") == packed_mtime + ): + return cache.get("value") + commit = _resolve_git_head_hash(root) + cache.update( + { + "source": "git", + "path": head_path, + "mtime": head_mtime, + "extra": packed_mtime, + "value": commit or None, + } + ) + if commit: + return commit + cache.update({"source": None, "path": None, "mtime": None, "extra": None, "value": None}) + return None except Exception: try: _AGENT_HASH_CACHE.update({"value": None}) @@ -803,6 +912,12 @@ def _build_details_fallback() -> dict: 'storage': collect_storage(), 'network': network, } + try: + agent_hash_value = _read_agent_hash() + if agent_hash_value: + details.setdefault('summary', {})['agent_hash'] = agent_hash_value + except Exception: + pass return details diff --git a/Data/Server/WebUI/src/Devices/Device_List.jsx b/Data/Server/WebUI/src/Devices/Device_List.jsx index 79f6fa9..faf415a 100644 --- a/Data/Server/WebUI/src/Devices/Device_List.jsx +++ b/Data/Server/WebUI/src/Devices/Device_List.jsx @@ -126,17 +126,15 @@ export default function DeviceList({ onSelectDevice }) { const fetchLatestRepoHash = useCallback(async () => { try { - const resp = await fetch( - "https://api.github.com/repos/bunny-lab-io/Borealis/branches/main", - { - headers: { - Accept: "application/vnd.github+json", - }, - } - ); - if (!resp.ok) throw new Error(`GitHub status ${resp.status}`); + const params = new URLSearchParams({ repo: "bunny-lab-io/Borealis", branch: "main" }); + const resp = await fetch(`/api/agent/repo_hash?${params.toString()}`); const json = await resp.json(); - const sha = (json?.commit?.sha || "").trim(); + const sha = (json?.sha || "").trim(); + if (!resp.ok || !sha) { + const err = new Error(`Latest hash status ${resp.status}${json?.error ? ` - ${json.error}` : ""}`); + err.response = json; + throw err; + } setRepoHash((prev) => sha || prev || null); return sha || null; } catch (err) { @@ -167,7 +165,7 @@ export default function DeviceList({ onSelectDevice }) { const arr = Object.entries(data || {}).map(([id, a]) => { const hostname = a.hostname || id || "unknown"; const details = detailsByHost[hostname] || {}; - const agentHash = (a.agent_hash || "").trim(); + const agentHash = (a.agent_hash || details.agentHash || "").trim(); return { id, hostname, @@ -230,43 +228,47 @@ export default function DeviceList({ onSelectDevice }) { const externalIp = summary.external_ip || ""; const lastReboot = summary.last_reboot || ""; const description = summary.description || ""; + const agentHashValue = (summary.agent_hash || "").trim(); + const enriched = { + lastUser, + created: createdRaw, + createdTs, + type: deviceType, + internalIp, + externalIp, + lastReboot, + description, + agentHash: agentHashValue, + }; setDetailsByHost((prev) => ({ ...prev, - [h]: { - lastUser, - created: createdRaw, - createdTs, - type: deviceType, - internalIp, - externalIp, - lastReboot, - description, - }, + [h]: enriched, })); + setRows((prev) => + prev.map((r) => { + if (r.hostname !== h) return r; + const nextHash = agentHashValue || r.agentHash; + return { + ...r, + lastUser: enriched.lastUser || r.lastUser, + type: enriched.type || r.type, + created: enriched.created || r.created, + createdTs: enriched.createdTs || r.createdTs, + internalIp: enriched.internalIp || r.internalIp, + externalIp: enriched.externalIp || r.externalIp, + lastReboot: enriched.lastReboot || r.lastReboot, + description: enriched.description || r.description, + agentHash: nextHash, + agentVersion: computeAgentVersion(nextHash, repoSha), + }; + }) + ); } catch { // ignore per-host failure } }) ); } - // After caching, refresh rows to apply newly available details - setRows((prev) => - prev.map((r) => { - const det = detailsByHost[r.hostname]; - if (!det) return r; - return { - ...r, - lastUser: det.lastUser || r.lastUser, - type: det.type || r.type, - created: det.created || r.created, - createdTs: det.createdTs || r.createdTs, - internalIp: det.internalIp || r.internalIp, - externalIp: det.externalIp || r.externalIp, - lastReboot: det.lastReboot || r.lastReboot, - description: det.description || r.description, - }; - }) - ); } } catch (e) { console.warn("Failed to load agents:", e); diff --git a/Data/Server/server.py b/Data/Server/server.py index e39b6ad..41608d6 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -21,6 +21,7 @@ from typing import List, Dict, Tuple, Optional, Any, Set import sqlite3 import io import uuid +from threading import Lock from datetime import datetime, timezone try: @@ -68,6 +69,142 @@ def _write_service_log(service: str, msg: str): pass +_REPO_HEAD_CACHE: Dict[str, Tuple[str, float]] = {} +_REPO_HEAD_LOCK = Lock() + +_DEFAULT_REPO = os.environ.get('BOREALIS_REPO', 'bunny-lab-io/Borealis') +_DEFAULT_BRANCH = os.environ.get('BOREALIS_REPO_BRANCH', 'main') +try: + _REPO_HASH_INTERVAL = int(os.environ.get('BOREALIS_REPO_HASH_REFRESH', '60')) +except ValueError: + _REPO_HASH_INTERVAL = 60 +_REPO_HASH_INTERVAL = max(30, min(_REPO_HASH_INTERVAL, 3600)) +_REPO_HASH_WORKER_STARTED = False +_REPO_HASH_WORKER_LOCK = Lock() + + +def _fetch_repo_head(owner_repo: str, branch: str = 'main', *, ttl_seconds: int = 60, force_refresh: bool = False) -> Dict[str, Any]: + """Resolve the latest commit hash for ``owner_repo``/``branch`` via GitHub's REST API. + + The server caches the response so that a fleet of agents can reuse the + result without exhausting rate limits. ``ttl_seconds`` bounds how long a + cached value is considered fresh. When ``force_refresh`` is True the cache + is bypassed and a new request is attempted immediately. + """ + + key = f"{owner_repo}:{branch}" + now = time.time() + + with _REPO_HEAD_LOCK: + cached = _REPO_HEAD_CACHE.get(key) + + cached_sha: Optional[str] = None + cached_ts: Optional[float] = None + cached_age: Optional[float] = None + if cached: + cached_sha, cached_ts = cached + cached_age = max(0.0, now - cached_ts) + + if cached_sha and not force_refresh and cached_age is not None and cached_age < max(30, ttl_seconds): + return { + 'sha': cached_sha, + 'cached': True, + 'age_seconds': cached_age, + 'error': None, + 'source': 'cache', + } + + headers = { + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'Borealis-Server' + } + token = os.environ.get('BOREALIS_GITHUB_TOKEN') or os.environ.get('GITHUB_TOKEN') + if token: + headers['Authorization'] = f'Bearer {token}' + + error_msg: Optional[str] = None + sha: Optional[str] = None + try: + resp = requests.get( + f'https://api.github.com/repos/{owner_repo}/branches/{branch}', + headers=headers, + timeout=20, + ) + if resp.status_code == 200: + data = resp.json() + sha = (data.get('commit') or {}).get('sha') + else: + error_msg = f'GitHub REST API repo head lookup failed: HTTP {resp.status_code} {resp.text[:200]}' + except Exception as exc: # pragma: no cover - defensive logging + error_msg = f'GitHub REST API repo head lookup raised: {exc}' + + if sha: + sha = sha.strip() + with _REPO_HEAD_LOCK: + _REPO_HEAD_CACHE[key] = (sha, now) + return { + 'sha': sha, + 'cached': False, + 'age_seconds': 0.0, + 'error': None, + 'source': 'github', + } + + if error_msg: + _write_service_log('server', error_msg) + + if cached_sha is not None: + return { + 'sha': cached_sha, + 'cached': True, + 'age_seconds': cached_age, + 'error': error_msg or 'using cached value', + 'source': 'cache-stale', + } + + return { + 'sha': None, + 'cached': False, + 'age_seconds': None, + 'error': error_msg or 'unable to resolve repository head', + 'source': 'github', + } + + +def _refresh_default_repo_hash(force: bool = False) -> Dict[str, Any]: + ttl = max(30, _REPO_HASH_INTERVAL) + try: + return _fetch_repo_head(_DEFAULT_REPO, _DEFAULT_BRANCH, ttl_seconds=ttl, force_refresh=force) + except Exception as exc: # pragma: no cover - defensive logging + _write_service_log('server', f'default repo hash refresh failed: {exc}') + raise + + +def _repo_hash_background_worker(): + interval = max(30, _REPO_HASH_INTERVAL) + # Fetch immediately, then sleep between refreshes + while True: + try: + _refresh_default_repo_hash(force=True) + except Exception: + # _refresh_default_repo_hash already logs details + pass + eventlet.sleep(interval) + + +def _ensure_repo_hash_worker(): + global _REPO_HASH_WORKER_STARTED + with _REPO_HASH_WORKER_LOCK: + if _REPO_HASH_WORKER_STARTED: + return + _REPO_HASH_WORKER_STARTED = True + try: + eventlet.spawn_n(_repo_hash_background_worker) + except Exception as exc: + _REPO_HASH_WORKER_STARTED = False + _write_service_log('server', f'failed to start repo hash worker: {exc}') + + def _ansible_log_server(msg: str): _write_service_log('ansible', msg) @@ -126,6 +263,8 @@ socketio = SocketIO( } ) +_ensure_repo_hash_worker() + # --------------------------------------------- # Serve ReactJS Production Vite Build from dist/ # --------------------------------------------- @@ -147,6 +286,44 @@ def serve_dist(path): def health(): return jsonify({"status": "ok"}) + +@app.route("/api/agent/repo_hash", methods=["GET"]) +def api_agent_repo_hash(): + try: + repo = (request.args.get('repo') or _DEFAULT_REPO).strip() + branch = (request.args.get('branch') or _DEFAULT_BRANCH).strip() + refresh_flag = (request.args.get('refresh') or '').strip().lower() + ttl_raw = request.args.get('ttl') + if '/' not in repo: + return jsonify({"error": "repo must be in the form owner/name"}), 400 + try: + ttl = int(ttl_raw) if ttl_raw else _REPO_HASH_INTERVAL + except ValueError: + ttl = _REPO_HASH_INTERVAL + ttl = max(30, min(ttl, 3600)) + force_refresh = refresh_flag in {'1', 'true', 'yes', 'force', 'refresh'} + if repo == _DEFAULT_REPO and branch == _DEFAULT_BRANCH: + result = _refresh_default_repo_hash(force=force_refresh) + else: + result = _fetch_repo_head(repo, branch, ttl_seconds=ttl, force_refresh=force_refresh) + sha = (result.get('sha') or '').strip() + payload = { + 'repo': repo, + 'branch': branch, + 'sha': sha if sha else None, + 'cached': bool(result.get('cached')), + 'age_seconds': result.get('age_seconds'), + 'source': result.get('source'), + } + if result.get('error'): + payload['error'] = result['error'] + if sha: + return jsonify(payload) + return jsonify(payload), 503 + except Exception as exc: + _write_service_log('server', f'/api/agent/repo_hash error: {exc}') + return jsonify({"error": "internal error"}), 500 + # --------------------------------------------- # Server Time Endpoint # ---------------------------------------------