mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
* fix(security): remove hardcoded secrets, patch command injection, tighten CORS and Cypher guard
- run_import_workflow.py, run_multi_import.py: replace hardcoded budget
password with BUDGET_PASSWORD env-var (fail-fast if unset)
- server.js /ingest: replace shell-interpolated exec() with execFile()
so user-controlled title/body cannot inject shell commands
- authentik-values.yaml: blank out bootstrap_password and bootstrap_token
so they must be supplied at deploy time via --set or sealed-secret
- cluster-dashboard main.py: restrict CORS from allow_origins=["*"] to
env-configurable whitelist (default: dashboard.researchstack.info),
methods to GET, headers to Authorization+Content-Type
- neo4j_obsidian_connector_router.js (both copies): replace permissive
prefix-only readOnly regex with a deny-list that blocks
CREATE/MERGE/DELETE/DETACH/SET/REMOVE/DROP anywhere in the query, and
route readOnly queries through session.readTransaction()
Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
* fix(security): add CALL procedure allowlist for Cypher readOnly, add OPTIONS to CORS
- Cypher guard: restore positive allowlist for CALL targets (only db.* and
apoc.meta.* allowed in readOnly mode). Extract cypherReadOnlyViolation()
helper for clarity. Both copies updated.
- CORS: add OPTIONS to allow_methods so preflight requests succeed.
Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
* fix(security): use negative lookahead for CALL allowlist, remove dead CALL\s*\{ branch
- Replace two-regex CALL check with single negative-lookahead
CYPHER_CALL_DISALLOWED_RE = /\bCALL\s+(?!db\.|apoc\.meta\.)/i
This correctly blocks queries containing ANY disallowed CALL target,
even when bundled alongside an allowed CALL db.* or CALL apoc.meta.*.
- Remove dead CALL\s*\{ alternative from CYPHER_WRITE_RE — the trailing
\b never matched because { is a non-word character.
- Both copies updated identically.
Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
import asyncio
|
|
from playwright.async_api import async_playwright
|
|
import os
|
|
|
|
PASSWORD = os.environ.get("BUDGET_PASSWORD")
|
|
if not PASSWORD:
|
|
raise SystemExit("Error: BUDGET_PASSWORD environment variable is not set.")
|
|
CSV_PATH = "/home/allaun/.gemini/antigravity/scratch/affirm_loans_import.csv"
|
|
SCREENSHOT_DIR = "/home/allaun/.gemini/antigravity/scratch/screenshots"
|
|
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
|
|
async def main():
|
|
async with async_playwright() as p:
|
|
print("[*] Launching browser...")
|
|
browser = await p.chromium.launch(headless=True)
|
|
context = await browser.new_context(ignore_https_errors=True)
|
|
page = await context.new_page()
|
|
|
|
# 1. Login
|
|
print("[*] Navigating to login page...")
|
|
await page.goto("https://budget.researchstack.info/login")
|
|
await page.wait_for_selector("input[type='password']")
|
|
await page.fill("input[type='password']", PASSWORD)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/01_login_filled.png")
|
|
await page.click("button:has-text('Sign in')")
|
|
await page.wait_for_timeout(5000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/02_after_login.png")
|
|
|
|
# 2. Open Budget File
|
|
print("[*] Opening 'My Finances' budget...")
|
|
await page.click("text=My Finances")
|
|
await page.wait_for_timeout(8000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/03_budget_loaded.png")
|
|
|
|
# 3. Check if 'Affirm Loans' already exists in the sidebar
|
|
sidebar_text = await page.evaluate("document.body.innerText")
|
|
if "Affirm Loans" in sidebar_text:
|
|
print("[+] 'Affirm Loans' account already exists in sidebar. Clicking it...")
|
|
await page.click("text=Affirm Loans")
|
|
await page.wait_for_timeout(4000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/04_account_opened.png")
|
|
else:
|
|
print("[*] Creating 'Affirm Loans' account...")
|
|
# Click Add account button (sidebar or main page)
|
|
add_account_buttons = page.locator("text=Add account")
|
|
await add_account_buttons.first.click()
|
|
await page.wait_for_timeout(3000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/04_add_account_modal.png")
|
|
|
|
# Click Create local account
|
|
await page.click("button:has-text('Create local account')")
|
|
await page.wait_for_timeout(3000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/05_local_account_form.png")
|
|
|
|
# Fill form
|
|
await page.fill("input[name='name']", "Affirm Loans")
|
|
await page.check("input[type='checkbox']#offbudget")
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/06_form_filled.png")
|
|
|
|
# Click Create
|
|
await page.locator("button:has-text('Create')").last.click()
|
|
await page.wait_for_timeout(6000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/07_after_account_created.png")
|
|
|
|
# 4. Trigger Import
|
|
print("[*] Triggering import...")
|
|
# Look for the Import button
|
|
import_btn = page.locator("button:has-text('Import'), a:has-text('Import'), span:has-text('Import')")
|
|
count = await import_btn.count()
|
|
print(f"[*] Found {count} potential import elements")
|
|
|
|
# Let's try to click the first visible/available Import button
|
|
async with page.expect_file_chooser() as fc_info:
|
|
await import_btn.first.click()
|
|
file_chooser = await fc_info.value
|
|
print(f"[+] Selecting file {CSV_PATH}...")
|
|
await file_chooser.set_files(CSV_PATH)
|
|
await page.wait_for_timeout(6000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/08_after_file_selected.png")
|
|
|
|
# 5. Confirm mapping and import
|
|
print("[*] Confirming import mapping...")
|
|
# Locate the button that finishes the import
|
|
# Usually it says "Import 21 transactions" or "Import"
|
|
final_btn = page.locator("button:has-text('transactions'), button:has-text('Import')")
|
|
final_count = await final_btn.count()
|
|
print(f"[*] Found {final_count} final import buttons")
|
|
|
|
await final_btn.last.click()
|
|
print("[*] Finalizing import...")
|
|
await page.wait_for_timeout(6000)
|
|
await page.screenshot(path=f"{SCREENSHOT_DIR}/09_import_finished.png")
|
|
|
|
# Get final status
|
|
final_text = await page.evaluate("document.body.innerText")
|
|
print("\n--- Final Page Body Text ---")
|
|
print(final_text[:2000])
|
|
print("----------------------------")
|
|
|
|
await browser.close()
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|