From 223dfd34e3c87660783b544021cda9f1189dd4ee Mon Sep 17 00:00:00 2001 From: Guillaume BOURGEOIS Date: Sun, 12 Jul 2026 15:23:56 +0200 Subject: [PATCH] feat(box): new Box file/evidence integration Box Content API v2, 6 commands: search, get file/folder info, list folder items, create shared link. Bearer-token auth, stdlib-only. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/box/manifest.yaml | 74 +++++++++++++++++++ .../box/scripts/create_shared_link.py | 58 +++++++++++++++ integrations/box/scripts/get_file_info.py | 51 +++++++++++++ integrations/box/scripts/get_folder_info.py | 51 +++++++++++++ integrations/box/scripts/list_folder_items.py | 52 +++++++++++++ integrations/box/scripts/search.py | 49 ++++++++++++ integrations/box/scripts/test_connection.py | 46 ++++++++++++ 7 files changed, 381 insertions(+) create mode 100644 integrations/box/manifest.yaml create mode 100644 integrations/box/scripts/create_shared_link.py create mode 100644 integrations/box/scripts/get_file_info.py create mode 100644 integrations/box/scripts/get_folder_info.py create mode 100644 integrations/box/scripts/list_folder_items.py create mode 100644 integrations/box/scripts/search.py create mode 100644 integrations/box/scripts/test_connection.py diff --git a/integrations/box/manifest.yaml b/integrations/box/manifest.yaml new file mode 100644 index 0000000..8b05756 --- /dev/null +++ b/integrations/box/manifest.yaml @@ -0,0 +1,74 @@ +id: box +name: Box +version: 1.0.0 +description: "Box (Content API v2) — evidence and file handling: search files, read file/folder metadata, list a folder's items, and create a shared link. Bearer-token authentication; stdlib-only, no extra Python dependencies." +changelog: "1.0.0 — Initial release: search, get file/folder info, list folder items, create shared link." +category: productivity + +# Per-instance configuration. Auth header 'Authorization: Bearer '. +config_schema: + properties: + access_token: + type: string + description: "Box access token (developer token or OAuth2/JWT-issued token)" + x-soar-sensitive: true + required: + - access_token + +commands: + - id: search + name: box-search + description: "Search for files and folders by keyword." + risk: read + inputs_schema: + properties: + query: { type: string, description: "Search query" } + limit: { type: number, description: "Max results (default 30)" } + required: [query] + outputs_schema: { properties: {} } + - id: get_file_info + name: box-get-file-info + description: "Get a file's metadata." + risk: read + inputs_schema: + properties: + file_id: { type: string, description: "File ID" } + required: [file_id] + outputs_schema: { properties: {} } + - id: get_folder_info + name: box-get-folder-info + description: "Get a folder's metadata." + risk: read + inputs_schema: + properties: + folder_id: { type: string, description: "Folder ID (0 = root)" } + required: [folder_id] + outputs_schema: { properties: {} } + - id: list_folder_items + name: box-list-folder-items + description: "List the items inside a folder." + risk: read + inputs_schema: + properties: + folder_id: { type: string, description: "Folder ID (0 = root)" } + limit: { type: number, description: "Max items (default 100)" } + required: [folder_id] + outputs_schema: { properties: {} } + - id: create_shared_link + name: box-create-shared-link + description: "Create a shared link for a file." + inputs_schema: + properties: + file_id: { type: string, description: "File ID" } + access: { type: string, description: "open, company, or collaborators (default company)" } + required: [file_id] + outputs_schema: { properties: {} } + + - id: test_connection + name: box-test-connection + description: "Verify the access token (used by the Test button)." + risk: read + inputs_schema: + properties: {} + required: [] + outputs_schema: { properties: {} } diff --git a/integrations/box/scripts/create_shared_link.py b/integrations/box/scripts/create_shared_link.py new file mode 100644 index 0000000..62230f4 --- /dev/null +++ b/integrations/box/scripts/create_shared_link.py @@ -0,0 +1,58 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + access = inputs.get("access") + return request( + "PUT", + "/files/" + q(file_id), + cfg, + body={"shared_link": {"access": (access or "company")}}, + params={"fields": "shared_link"}, + ) + + +_run(main) diff --git a/integrations/box/scripts/get_file_info.py b/integrations/box/scripts/get_file_info.py new file mode 100644 index 0000000..848491c --- /dev/null +++ b/integrations/box/scripts/get_file_info.py @@ -0,0 +1,51 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + file_id = inputs.get("file_id") + if not file_id: + raise Exception("file_id is required") + return request("GET", "/files/" + q(file_id), cfg) + + +_run(main) diff --git a/integrations/box/scripts/get_folder_info.py b/integrations/box/scripts/get_folder_info.py new file mode 100644 index 0000000..0efd053 --- /dev/null +++ b/integrations/box/scripts/get_folder_info.py @@ -0,0 +1,51 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + folder_id = inputs.get("folder_id") + if not folder_id: + raise Exception("folder_id is required") + return request("GET", "/folders/" + q(folder_id), cfg) + + +_run(main) diff --git a/integrations/box/scripts/list_folder_items.py b/integrations/box/scripts/list_folder_items.py new file mode 100644 index 0000000..680722f --- /dev/null +++ b/integrations/box/scripts/list_folder_items.py @@ -0,0 +1,52 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +q = lambda v: urllib.parse.quote(str(v), safe="") + + +def main(cfg, inputs): + folder_id = inputs.get("folder_id") + if not folder_id: + raise Exception("folder_id is required") + limit = inputs.get("limit") + return request("GET", "/folders/" + q(folder_id) + "/items", cfg, params={"limit": int(limit or 100)}) + + +_run(main) diff --git a/integrations/box/scripts/search.py b/integrations/box/scripts/search.py new file mode 100644 index 0000000..236555a --- /dev/null +++ b/integrations/box/scripts/search.py @@ -0,0 +1,49 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(cfg, inputs): + query = inputs.get("query") + if not query: + raise Exception("query is required") + limit = inputs.get("limit") + return request("GET", "/search", cfg, params={"query": query, "limit": int(limit or 30)}) + + +_run(main) diff --git a/integrations/box/scripts/test_connection.py b/integrations/box/scripts/test_connection.py new file mode 100644 index 0000000..04264e5 --- /dev/null +++ b/integrations/box/scripts/test_connection.py @@ -0,0 +1,46 @@ +import json, os, sys, urllib.parse, urllib.request, urllib.error + +BASE = "https://api.box.com/2.0" + + +def _cfg(): + return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}")) + + +def _inputs(): + return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}")) + + +def request(method, path, cfg, body=None, params=None): + url = BASE + path + if params: + clean = {k: v for k, v in params.items() if v not in (None, "")} + if clean: + url += "?" + urllib.parse.urlencode(clean) + data = json.dumps(body).encode("utf-8") if body is not None else None + headers = {"Authorization": "Bearer " + str(cfg.get("access_token", "")), "Accept": "application/json"} + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=60) as r: + raw = r.read() + return json.loads(raw) if raw else {} + + +def _run(fn): + try: + print(json.dumps(fn(_cfg(), _inputs()))) + except urllib.error.HTTPError as e: + print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")})) + sys.exit(1) + except Exception as e: + print(json.dumps({"error": str(e)})) + sys.exit(1) + + +def main(cfg, inputs): + resp = request("GET", "/users/me", cfg) + return {"ok": True, "login": resp.get("login")} + + +_run(main)