diff --git a/Gemfile b/Gemfile index 7eac79b..40a7410 100644 --- a/Gemfile +++ b/Gemfile @@ -3,9 +3,12 @@ source "https://rubygems.org" ruby "3.3.6" gem "bootsnap", require: false +gem "json", ">= 2.21.2", "< 3" # fixes CVE-2026-71847; pinned below 3.x, which broke JSON.parse elsewhere +gem "loofah", ">= 2.25.2" # fixes GHSA-5qhf-9phg-95m2 / GHSA-8whx-365g-h9vv / CVE-2026-73490 (javascript: URI sanitizer bypasses) gem "pg", "~> 1.5" gem "puma", "~> 8.0" # >= 8.0.2 fixes CVE-2026-47736 / CVE-2026-47737 (PROXY protocol memory exhaustion) -gem "rails", "~> 8.0" +gem "rails", ">= 8.1.3.1", "< 8.2" # >= 8.1.3.1 fixes CVE-2026-66066 (Active Storage variant processing file read/RCE) +gem "rails-html-sanitizer", ">= 1.7.1" # fixes CVE-2026-73648 (possible XSS with certain sanitizer configurations) # Auth gem "devise", "~> 5.0" # >= 5.0.4 fixes CVE-2026-32700 (email-confirm race) / CVE-2026-40295 (open redirect) diff --git a/Gemfile.lock b/Gemfile.lock index d21ef92..b98b49c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,29 +3,29 @@ GEM specs: action_text-trix (2.1.19) railties - actioncable (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) - actionmailer (8.1.3) - actionpack (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activesupport (= 8.1.3) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.3) - actionview (= 8.1.3) - activesupport (= 8.1.3) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.3) + actiontext (8.1.3.1) action_text-trix (~> 2.1.15) - actionpack (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.3) - activesupport (= 8.1.3) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.3) - activesupport (= 8.1.3) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (8.1.3) - activesupport (= 8.1.3) - activerecord (8.1.3) - activemodel (= 8.1.3) - activesupport (= 8.1.3) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activesupport (= 8.1.3) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (8.1.3) + activesupport (8.1.3.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -168,7 +168,7 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.20.0) + json (2.21.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) listen (3.10.0) @@ -176,7 +176,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.25.1) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.1) @@ -252,30 +252,30 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.3) - actioncable (= 8.1.3) - actionmailbox (= 8.1.3) - actionmailer (= 8.1.3) - actionpack (= 8.1.3) - actiontext (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activemodel (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 8.1.3) + railties (= 8.1.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.7.0) - loofah (~> 2.25) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -328,8 +328,8 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.7) - rubocop (1.88.1) - json (~> 2.3) + rubocop (1.91.0) + json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) parallel (>= 1.10) @@ -411,13 +411,16 @@ DEPENDENCIES factory_bot_rails (~> 6.4) faker (~> 3.3) jbuilder (~> 2.12) + json (>= 2.21.2, < 3) listen (~> 3.8) + loofah (>= 2.25.2) pg (~> 1.5) puma (~> 8.0) pundit (~> 2.3) rack-attack (~> 6.7) rack-cors (~> 2.0) - rails (~> 8.0) + rails (>= 8.1.3.1, < 8.2) + rails-html-sanitizer (>= 1.7.1) redis (~> 5.1) rspec (~> 3.13) rspec-rails (~> 6.1) @@ -431,17 +434,17 @@ DEPENDENCIES CHECKSUMS action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009 - actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 - actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 - actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d - actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 - actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 - actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d - activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 - activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 - activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab - activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d - activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + actioncable (8.1.3.1) sha256=e318528295c878a3efdfe25f0f2267c80cb7a76eba41bb5f64d44aa380a3d91b + actionmailbox (8.1.3.1) sha256=5f704972097d843ade8e435e93694a1dac732b926df1717aceba1f3840082b1c + actionmailer (8.1.3.1) sha256=88ea441b28ff02a0c6c006468892642a3d9942affce9d294e81a74504aa5c43c + actionpack (8.1.3.1) sha256=974cb7154548e81f470b1b0f247b99cb38e87825899dca58610596e2817723d0 + actiontext (8.1.3.1) sha256=5da729d833d1a29cddb1eee938878e55e503d2613e00e735f5daf58c2ba98af2 + actionview (8.1.3.1) sha256=2da68b8414c47b43bfbed1ce69c5afe1c04f78c267aacb5660a4cab5ca12cfb6 + activejob (8.1.3.1) sha256=1c8dd275df930df40deecffec63d913a550a33fd94bd298f69721dd96939954a + activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88 + activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b + activestorage (8.1.3.1) sha256=f555254f387b1cffa499d2fd3115d12635eadc5b15206a8534316a67036163ef + activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376 addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b @@ -491,12 +494,12 @@ CHECKSUMS irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 jbuilder (2.15.1) sha256=2430bec28fb0cebacb5875b1009cf9d8bc3c303ccb810c4c8b062a4b51457637 jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 - json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef @@ -539,10 +542,10 @@ CHECKSUMS rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 - rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails (8.1.3.1) sha256=ccd11a36bfc171bf9c66d585d14c0ece91c0c9dde840aae60c0118d6f5c9c52a rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 - railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe @@ -561,7 +564,7 @@ CHECKSUMS rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-rails (6.1.5) sha256=d11afce893ceb6e2c3c11db280f83dee6d0120d150228cef6b989d37c7394c4b rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c - rubocop (1.88.1) sha256=726af773d6bc169ed3ff852f3ca020b7c58d39c34e7a8d879a8e0147cc994f26 + rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0 rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-rails (2.35.5) sha256=f00b3c936002ba8e9ac62e8607c54bb24cda44b36e41b9c7e4f3872e1b0f3fe3 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 diff --git a/app/models/api_token.rb b/app/models/api_token.rb index 9c49abc..64a7dd5 100644 --- a/app/models/api_token.rb +++ b/app/models/api_token.rb @@ -78,9 +78,7 @@ def record_use!(ip:) # update_columns deliberately skips validations/callbacks: this runs on # every authenticated request and must not re-validate the whole # record (e.g. expires_at_within_max_ttl) just to bump a usage marker. - # rubocop:disable Rails/SkipsModelValidations - update_columns(last_used_at: Time.current, last_used_ip: ip) - # rubocop:enable Rails/SkipsModelValidations + update_columns(last_used_at: Time.current, last_used_ip: ip) # rubocop:disable Rails/SkipsModelValidations end private diff --git a/tools/apps-script-backend/Code.gs b/tools/apps-script-backend/Code.gs new file mode 100644 index 0000000..de67df6 --- /dev/null +++ b/tools/apps-script-backend/Code.gs @@ -0,0 +1,155 @@ +/** + * Shared logging backend for ZeitHawk and AI Tool Finder. + * + * Both artifacts open a mailto: link for "Request Security authorization" / + * "Request Security Approval" / "Request IT review" — that's the real + * request path and works with zero setup. This web app additionally logs + * a durable copy of each request to Firestore, so there's a searchable + * record beyond whatever lands in one inbox. + * + * Deployment — see README.md in this folder for the full walkthrough: + * 1. Associate this Apps Script project with a standard GCP project + * (Project Settings -> Google Cloud Platform (GCP) Project) and + * enable the Firestore API on it, in Native mode. + * 2. Set PROJECT_ID below to that GCP project's ID. + * 3. Deploy -> New deployment -> Web app, execute as "Me", + * access "Anyone" (the artifacts call this anonymously). + * 4. Paste the resulting /exec URL into APPS_SCRIPT_URL in both + * tools/zeithawk/index.html and the AI Tool Finder artifact. + * + * This endpoint has no auth — "Anyone" access means anyone with the URL + * can write to it. That's an accepted tradeoff for logging low-sensitivity + * *requests* (never put secrets in the fields below), not a security + * boundary. See README.md for the caveats in more detail. + */ + +const PROJECT_ID = 'REPLACE_WITH_YOUR_GCP_PROJECT_ID'; +const DATABASE_ID = '(default)'; +const ALLOWED_COLLECTIONS = ['security_authorization_requests', 'app_review_requests']; +const MAX_LIST_LIMIT = 50; + +function doPost(e) { + try { + const payload = JSON.parse(e.postData.contents); + const collection = payload.collection; + if (ALLOWED_COLLECTIONS.indexOf(collection) === -1) { + return jsonOutput_({ ok: false, error: 'Unknown collection: ' + collection }); + } + const doc = { + tool: String(payload.tool || '').slice(0, 200), + target: String(payload.target || '').slice(0, 500), + requester: String(payload.requester || '').slice(0, 200), + note: String(payload.note || '').slice(0, 2000), + createdAt: new Date().toISOString(), + }; + const created = firestoreCreateDocument_(collection, doc); + return jsonOutput_({ ok: true, id: created.name.split('/').pop() }); + } catch (err) { + return jsonOutput_({ ok: false, error: String(err) }); + } +} + +function doGet(e) { + const params = (e && e.parameter) || {}; + const collection = params.collection; + const callback = params.callback; + let result; + + if (ALLOWED_COLLECTIONS.indexOf(collection) === -1) { + result = { ok: false, error: 'Unknown collection: ' + collection }; + } else { + const limit = Math.min(parseInt(params.limit, 10) || 20, MAX_LIST_LIMIT); + try { + result = { ok: true, items: firestoreListDocuments_(collection, limit) }; + } catch (err) { + result = { ok: false, error: String(err) }; + } + } + + // Apps Script web app responses don't carry CORS headers, so a plain + // cross-origin fetch() can't read the body. Callers that need to read + // the result (rather than fire-and-forget a POST) pass ?callback=name + // and get JSONP back via a