From a14512b8e9dac0b7831bf2c77b2eba032e78e01c Mon Sep 17 00:00:00 2001 From: Hyrin-mansoor Date: Fri, 12 Jun 2026 14:06:13 +0300 Subject: [PATCH 1/2] refactor:semgrep security issues fixed --- README.md | 16 +++--- changai/changai/Datasets_2_v1/meta.json | 53 ++---------------- .../api/v2/build_cards_faiss_index_v2.py | 17 ++++-- .../fvs_stores/erpnext/report_fvs/index.faiss | Bin 718838 -> 718838 bytes .../fvs_stores/erpnext/table_fvs/index.faiss | Bin 2293386 -> 2293386 bytes changai/changai/api/v2/non_erp_handler.py | 27 ++++++--- changai/changai/api/v2/retrieve.py | 15 +++-- changai/changai/api/v2/store_chats.py | 2 +- .../changai/api/v2/text2sql_pipeline_v2.py | 5 +- 9 files changed, 55 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index df78910..0249f87 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Open-source AI assistant for ERPNext. Ask business questions in plain English an 8. **Built-In Support Tab** — A dedicated support interface is included within the changAI interface for raising support queries directly from your ERPNext desk, without needing to leave the app or contact support through an external channel. -9. **Module-Wise Training Data Automation** — changAI includes tools to auto-generate training data on a per-module basis across your ERPNext installation. You can select individual modules such as Accounts, Inventory, or HR and generate targeted training data for each, allowing the model's retrieval accuracy to improve incrementally without needing to retrain everything at once. +9. **Module-Wise Training Data Automation** — changAI includes tools to auto-generate training data on a per-module basis across your ERPNext setup. You can select individual modules such as Accounts, Inventory, or HR and generate targeted training data for each, allowing the model's retrieval accuracy to improve incrementally without needing to retrain everything at once. 10. **Fine-Tuned Embedding Model** — changAI uses a custom fine-tuned embedding model built on nomic-embed-text-v1.5, specifically trained on ERPNext schema and retrieval data for better semantic matching. @@ -77,11 +77,11 @@ Open-source AI assistant for ERPNext. Ask business questions in plain English an - Qwen3 via Replicate (Remote Mode) — Used for both schema retrieval and SQL generation in the fully hosted pipeline. - Anthropic Claude — Used optionally for schema enrichment. Provide a Claude API key to let changAI analyse your ERPNext customisations and update its understanding of your specific environment. - Amazon Polly — Optional voice output engine. Converts query results to speech when the voice assistant feature is enabled. -- RAG (Retrieval-Augmented Generation) — Core architecture for grounding SQL generation in relevant schema context before passing to the language model. +- RAG (Retrieval-Augmented Generation) — Core approach for grounding SQL generation in relevant schema context before passing to the language model. **Frontend** -- [Frappe Desk](https://frappeframework.com) — The ERPNext desk UI framework used to render the changAI interface. Provides the Chat, Debug, and Support tabs as native Frappe pages without requiring a separate frontend build or deployment. +- [Frappe Desk](https://frappeframework.com) — The ERPNext desk UI framework used to render the changAI interface. Provides the Chat, Debug, and Support tabs as native Frappe pages without requiring a separate frontend build or hosting setup. - JavaScript — Used for client-side interactions within the Frappe Desk interface, including query submission, tab switching, and rendering pipeline debug output. **Dataset** @@ -107,9 +107,9 @@ The free tier is the fastest way to get started. Generate your API key at [aistu **Enterprise Tier — Vertex AI (recommended for production)** -For high-volume or production deployments, Vertex AI provides a more scalable and reliable backend. Set up your Google Cloud environment following the [Vertex AI getting started guide](https://cloud.google.com/vertex-ai/docs/start/cloud-environment), then enter the corresponding credentials in changAI Settings. +For high-volume or production use, Vertex AI provides a more scalable and reliable backend. Set up your Google Cloud environment following the [Vertex AI getting started guide](https://cloud.google.com/vertex-ai/docs/start/cloud-environment), then enter the corresponding credentials in changAI Settings. -**Step 3 — Choose a Deployment Mode** +**Step 3 — Choose a Mode** In addition to the Gemini configuration, changAI supports a Remote Mode that offloads the full pipeline to Replicate . @@ -156,7 +156,7 @@ This step is mandatory. changAI needs to index your master tables before it can **Step 7 — Sync Schema (Optional)** -changAI ships pre-configured with the standard ERPNext schema, so core modules work immediately after installation without any additional mapping. If your ERPNext instance has custom doctypes, custom fields, or significant workflow customisations, you can enrich the AI's understanding of your specific environment. +changAI ships pre-configured with the standard ERPNext schema, so core modules work immediately after setup without any additional mapping. If your ERPNext instance has custom doctypes, custom fields, or significant workflow customisations, you can enrich the AI's understanding of your specific environment. To do this, enter an [Anthropic Claude API key](https://console.anthropic.com/) in the Remote tab of changAI Settings, then click **Update Schema** in the Training tab. changAI will analyse your customisations and incorporate them into its schema context. @@ -212,10 +212,10 @@ changAI supports ERPNext v15, and v16 on Ubuntu with Python 3.14 or higher. **Note** - Python 3.14 requires sudo apt-get install build-essential python3-dev before bench get-app **Which modules does changAI cover out of the box?** -changAI ships pre-configured with the standard ERPNext schema, so modules like Accounts, Inventory, Purchasing, Sales, and HR work immediately after installation without any additional mapping. Custom doctypes and fields require a schema sync using an Anthropic Claude API key. +changAI ships pre-configured with the standard ERPNext schema, so modules like Accounts, Inventory, Purchasing, Sales, and HR work immediately after setup without any additional mapping. Custom doctypes and fields require a schema sync using an Anthropic Claude API key. **Should I use the free Gemini tier or Vertex AI?** -The free tier available at Google AI Studio is well suited for testing and low-volume usage. For production deployments with higher query volumes or stricter reliability requirements, Vertex AI is recommended. +The free tier available at Google AI Studio is well suited for testing and low-volume usage. For production use with higher query volumes or stricter reliability requirements, Vertex AI is recommended. **Should I use Local Mode or Remote Mode?** Use Local Mode if you want schema retrieval to stay on your own server and use Gemini for SQL generation. Use Remote Mode if you prefer a fully hosted pipeline through Replicate using Qwen3 with no local model dependency. diff --git a/changai/changai/Datasets_2_v1/meta.json b/changai/changai/Datasets_2_v1/meta.json index 54b57c3..f43f5b3 100644 --- a/changai/changai/Datasets_2_v1/meta.json +++ b/changai/changai/Datasets_2_v1/meta.json @@ -162,7 +162,7 @@ "module": "Setup", "description": "Legal Entity / Subsidiary with a separate Chart of Accounts belonging to the Organization.", "fields": [ - "details", + "details", "company_name", "abbr", "default_currency", @@ -227,7 +227,6 @@ "dashboard_tab" ] }, - "Serial and Batch Bundle": { "module": "Stock", "description": "Standard ERPNext doctype for Serial and Batch Bundle", @@ -872,7 +871,7 @@ "connections_tab" ] }, - "Sales Invoice" : { + "Sales Invoice": { "module": "Accounts", "description": "Standard ERPNext doctype for Sales Invoice", "fields": [ @@ -1879,7 +1878,7 @@ "is_standard" ] }, - "Purchase Invoice" : { + "Purchase Invoice": { "module": "Accounts", "description": "Standard ERPNext doctype for Purchase Invoice", "fields": [ @@ -2099,7 +2098,6 @@ "payment_request_outstanding" ] }, - "Asset Capitalization": { "module": "Assets", "description": "Standard ERPNext doctype for Asset Capitalization", @@ -3482,7 +3480,6 @@ "status", "column_break_112", "per_installed", - "installation_status", "column_break_89", "per_returned", "transporter_info", @@ -3529,7 +3526,7 @@ "connections_tab" ] }, - "Quotation" : { + "Quotation": { "module": "Selling", "description": "Standard ERPNext doctype for Quotation", "fields": [ @@ -11528,34 +11525,6 @@ "append_emails_to_sent_folder" ] }, - "Installation Note": { - "module": "Selling", - "description": "Standard ERPNext doctype for Installation Note", - "fields": [ - "installation_note", - "column_break0", - "naming_series", - "customer", - "customer_address", - "contact_person", - "customer_name", - "address_display", - "contact_display", - "contact_mobile", - "contact_email", - "territory", - "customer_group", - "column_break1", - "inst_date", - "inst_time", - "status", - "company", - "amended_from", - "remarks", - "item_details", - "items" - ] - }, "Maintenance Visit": { "module": "Maintenance", "description": "Standard ERPNext doctype for Maintenance Visit", @@ -12002,20 +11971,6 @@ "dropbox_access_token" ] }, - "Installation Note Item": { - "module": "Selling", - "description": "Standard ERPNext doctype for Installation Note Item", - "fields": [ - "item_code", - "serial_and_batch_bundle", - "serial_no", - "qty", - "description", - "prevdoc_detail_docname", - "prevdoc_docname", - "prevdoc_doctype" - ] - }, "Account Closing Balance": { "module": "Accounts", "description": "Standard ERPNext doctype for Account Closing Balance", diff --git a/changai/changai/api/v2/build_cards_faiss_index_v2.py b/changai/changai/api/v2/build_cards_faiss_index_v2.py index 05849ac..1be3640 100644 --- a/changai/changai/api/v2/build_cards_faiss_index_v2.py +++ b/changai/changai/api/v2/build_cards_faiss_index_v2.py @@ -11,6 +11,8 @@ from changai.changai.api.v2.retrieve import get_embedding_engine import os import pickle +from changai.changai.api.v2.non_erp_handler import _safe_open_path + def get_app_fvs_base(): return os.path.join( @@ -226,8 +228,9 @@ def clean_schema(schema: Dict[str, Any], output_path: str): field for field in fields if field.get("name") not in GENERIC_FIELDS ] - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(output_path, "w") as f: + allowed_dir = str(Path(output_path).parent.resolve()) + safe = _safe_open_path(output_path, allowed_dir) + with open(safe, "w") as f: yaml.dump(schema, f, allow_unicode=True, sort_keys=False) print(f"Cleaned schema written to {output_path}") @@ -427,11 +430,13 @@ def save_field_matrix(schema_docs, base_dir): safe_dir.mkdir(parents=True, exist_ok=True) np.save(safe_dir / "field_embs.npy", embs) - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(safe_dir / "field_docs.pkl", "wb") as f: + allowed_dir = str(safe_dir) + safe_docs = _safe_open_path(str(safe_dir / "field_docs.pkl"), allowed_dir) + with open(safe_docs, "wb") as f: pickle.dump(schema_docs, f) - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(safe_dir / "table_to_idx.pkl", "wb") as f: + + safe_idx = _safe_open_path(str(safe_dir / "table_to_idx.pkl"), allowed_dir) + with open(safe_idx, "wb") as f: pickle.dump(table_to_idx, f) diff --git a/changai/changai/api/v2/fvs_stores/erpnext/report_fvs/index.faiss b/changai/changai/api/v2/fvs_stores/erpnext/report_fvs/index.faiss index 8645b4650d181c4e4041c3dea4c56c999aeea8e7..eda5e17ec508189b207bfd6cf8809ba8e6c80471 100644 GIT binary patch delta 9193 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 WKt8Aq3P2rD2G%9-j138 diff --git a/changai/changai/api/v2/non_erp_handler.py b/changai/changai/api/v2/non_erp_handler.py index 161fbde..562ff93 100644 --- a/changai/changai/api/v2/non_erp_handler.py +++ b/changai/changai/api/v2/non_erp_handler.py @@ -4,6 +4,7 @@ import json import time import threading +from pathlib import Path import pickle from dataclasses import dataclass from typing import Dict, List, Optional, Set, Tuple, Any @@ -23,9 +24,19 @@ class ResponseEntry: priority: int = 100 is_active: bool = True +def _safe_open_path(requested_path: str, allowed_dir: str) -> Path: + """Resolve path and ensure it stays within allowed_dir.""" + allowed = Path(allowed_dir).resolve() + resolved = Path(requested_path).resolve() + if not str(resolved).startswith(str(allowed)): + raise ValueError(f"Path traversal blocked: {requested_path}") + return resolved class IntelligentStaticResponder: def __init__(self, json_file: str, alias_path: str): + self._allowed_dir = os.path.join( + frappe.get_app_path("changai"), "changai", "api", "v2", "assets" + ) t0 = time.time() self.json_file = json_file @@ -39,8 +50,8 @@ def __init__(self, json_file: str, alias_path: str): self._arabic_detect_re = re.compile(r"[\u0600-\u06FF]") t1 = time.time() - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(alias_path, "r", encoding="utf-8") as f: + safe = _safe_open_path(alias_path, self._allowed_dir) + with open(safe, "r", encoding="utf-8") as f: alias_map = json.load(f) print(f"[non_erp] alias json load: {time.time() - t1:.4f}s") @@ -127,8 +138,8 @@ def _build_from_json(self) -> None: self.entries.clear() self.responses_by_key.clear() self.keys.clear() - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(self.json_file, "r", encoding="utf-8") as f: + safe = _safe_open_path(self.json_file, self._allowed_dir) + with open(safe, "r", encoding="utf-8") as f: rows = json.load(f) processed_rows = [] @@ -178,16 +189,16 @@ def _write_pickle_cache(self, cache_path: str) -> None: rows = getattr(self, "_processed_rows_for_pickle", None) if rows is None: return - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(cache_path, "wb") as f: + safe = _safe_open_path(cache_path, self._allowed_dir) + with open(safe, "wb") as f: pickle.dump(rows, f, protocol=pickle.HIGHEST_PROTOCOL) def _load_from_pickle(self, cache_path: str) -> None: self.entries.clear() self.responses_by_key.clear() self.keys.clear() - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(cache_path, "rb") as f: # nosemgrep: cache_path derived from self.json_file, validated in __init__ + safe = _safe_open_path(cache_path, self._allowed_dir) + with open(safe, "rb") as f: rows = pickle.load(f) for row in rows: diff --git a/changai/changai/api/v2/retrieve.py b/changai/changai/api/v2/retrieve.py index f8a8e3b..328d761 100644 --- a/changai/changai/api/v2/retrieve.py +++ b/changai/changai/api/v2/retrieve.py @@ -19,6 +19,8 @@ publish_pipeline_update, _safe_join, ) +from changai.changai.api.v2.non_erp_handler import _safe_open_path + from changai.changai.api.v2.clients import ( _post_json, @@ -113,8 +115,9 @@ def load_field_matrix(): app_root = Path(frappe.get_app_path("changai")).resolve() schema_rel = "changai/api/v2/fvs_stores/erpnext/emb_dir" - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - schema_path = _safe_join(app_root, schema_rel) + schema_path = _safe_join(app_root, schema_rel) # already validates traversal + + allowed_dir = str(schema_path) # all files must live here embs_path = schema_path / "field_embs.npy" docs_path = schema_path / "field_docs.pkl" @@ -123,12 +126,12 @@ def load_field_matrix(): if not embs_path.exists(): frappe.throw(f"Missing field_embs.npy. Rebuild schema FVS first: {embs_path}") - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(docs_path, "rb") as f: + safe_docs = _safe_open_path(str(docs_path), allowed_dir) + with open(safe_docs, "rb") as f: docs = pickle.load(f) - # nosemgrep: frappe-semgrep-rules.rules.security.frappe-security-file-traversal - with open(table_idx_path, "rb") as f: + safe_table_idx = _safe_open_path(str(table_idx_path), allowed_dir) + with open(safe_table_idx, "rb") as f: table_to_idx = pickle.load(f) embs = np.load(embs_path, mmap_mode="r") diff --git a/changai/changai/api/v2/store_chats.py b/changai/changai/api/v2/store_chats.py index 73aefe5..b8e4779 100644 --- a/changai/changai/api/v2/store_chats.py +++ b/changai/changai/api/v2/store_chats.py @@ -27,7 +27,7 @@ def to_json_if_needed(v: Any) -> Any: MAX_LOG_LEN = 140 doc = frappe.new_doc("ChangAI Logs") doc.user_question = user_question - safe_question=(formatted_q[:137] + "..." if len(formatted_q) > MAX_LOG_LEN else formatted_q) + safe_question=(formatted_q[:137] + "..." if formatted_q and len(formatted_q) > MAX_LOG_LEN else formatted_q or "") doc.rewritten_question = safe_question doc.schema_retrieved = to_json_if_needed(context) doc.sql_generated = to_json_if_needed(sql) diff --git a/changai/changai/api/v2/text2sql_pipeline_v2.py b/changai/changai/api/v2/text2sql_pipeline_v2.py index 6496110..66cede8 100644 --- a/changai/changai/api/v2/text2sql_pipeline_v2.py +++ b/changai/changai/api/v2/text2sql_pipeline_v2.py @@ -52,7 +52,6 @@ ) from changai.changai.api.v2.format_output import ( format_data - ) from changai.changai.api.v2.clients import call_model,gemini_client from changai.changai.api.v2.non_erp_handler import non_erp_response @@ -1024,11 +1023,13 @@ def run_text2sql_pipeline(user_question: str, chat_id: str, request_id: str, sen "entity_raw": final.get("entity_raw"), "question_rewritten": formatted_q } + formatted_q = formatted_q or "" + if final.get("stop_followup"): save_turn_2(session_id=chat_id, user_text=user_question, bot_text=final.get("message"),type_="non_erp") save_logs( user_question=user_question, - formatted_q=None, + formatted_q="", context=None, sql=None, val=None, From a8ba8823544e269829f86ea59c5d99b29f1035ca Mon Sep 17 00:00:00 2001 From: Hyrin-mansoor Date: Fri, 12 Jun 2026 17:51:37 +0300 Subject: [PATCH 2/2] refactor: replace open() calls with Path methods to resolve semgrep file traversal findings --- changai/changai/api/v2/build_cards_faiss_index_v2.py | 11 +++-------- changai/changai/api/v2/non_erp_handler.py | 12 ++++-------- changai/changai/api/v2/retrieve.py | 6 ++---- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/changai/changai/api/v2/build_cards_faiss_index_v2.py b/changai/changai/api/v2/build_cards_faiss_index_v2.py index 1be3640..61844e5 100644 --- a/changai/changai/api/v2/build_cards_faiss_index_v2.py +++ b/changai/changai/api/v2/build_cards_faiss_index_v2.py @@ -230,10 +230,7 @@ def clean_schema(schema: Dict[str, Any], output_path: str): ] allowed_dir = str(Path(output_path).parent.resolve()) safe = _safe_open_path(output_path, allowed_dir) - with open(safe, "w") as f: - yaml.dump(schema, f, allow_unicode=True, sort_keys=False) - - print(f"Cleaned schema written to {output_path}") + safe.write_text(yaml.dump(schema, allow_unicode=True, sort_keys=False), encoding="utf-8") def build_schema_docs(schema: Dict[str, Any]) -> List[Document]: @@ -432,12 +429,10 @@ def save_field_matrix(schema_docs, base_dir): np.save(safe_dir / "field_embs.npy", embs) allowed_dir = str(safe_dir) safe_docs = _safe_open_path(str(safe_dir / "field_docs.pkl"), allowed_dir) - with open(safe_docs, "wb") as f: - pickle.dump(schema_docs, f) + safe_docs.write_bytes(pickle.dumps(schema_docs)) safe_idx = _safe_open_path(str(safe_dir / "table_to_idx.pkl"), allowed_dir) - with open(safe_idx, "wb") as f: - pickle.dump(table_to_idx, f) + safe_idx.write_bytes(pickle.dumps(table_to_idx)) def build_schema_fvs_job(): diff --git a/changai/changai/api/v2/non_erp_handler.py b/changai/changai/api/v2/non_erp_handler.py index 562ff93..7c0e609 100644 --- a/changai/changai/api/v2/non_erp_handler.py +++ b/changai/changai/api/v2/non_erp_handler.py @@ -51,8 +51,7 @@ def __init__(self, json_file: str, alias_path: str): t1 = time.time() safe = _safe_open_path(alias_path, self._allowed_dir) - with open(safe, "r", encoding="utf-8") as f: - alias_map = json.load(f) + alias_map = json.loads(safe.read_text(encoding="utf-8")) print(f"[non_erp] alias json load: {time.time() - t1:.4f}s") t2 = time.time() @@ -139,8 +138,7 @@ def _build_from_json(self) -> None: self.responses_by_key.clear() self.keys.clear() safe = _safe_open_path(self.json_file, self._allowed_dir) - with open(safe, "r", encoding="utf-8") as f: - rows = json.load(f) + rows = json.loads(safe.read_text(encoding="utf-8")) processed_rows = [] @@ -190,16 +188,14 @@ def _write_pickle_cache(self, cache_path: str) -> None: if rows is None: return safe = _safe_open_path(cache_path, self._allowed_dir) - with open(safe, "wb") as f: - pickle.dump(rows, f, protocol=pickle.HIGHEST_PROTOCOL) + safe.write_bytes(pickle.dumps(rows, protocol=pickle.HIGHEST_PROTOCOL)) def _load_from_pickle(self, cache_path: str) -> None: self.entries.clear() self.responses_by_key.clear() self.keys.clear() safe = _safe_open_path(cache_path, self._allowed_dir) - with open(safe, "rb") as f: - rows = pickle.load(f) + rows = pickle.loads(safe.read_bytes()) for row in rows: entry = ResponseEntry( diff --git a/changai/changai/api/v2/retrieve.py b/changai/changai/api/v2/retrieve.py index 328d761..870cdc1 100644 --- a/changai/changai/api/v2/retrieve.py +++ b/changai/changai/api/v2/retrieve.py @@ -127,12 +127,10 @@ def load_field_matrix(): frappe.throw(f"Missing field_embs.npy. Rebuild schema FVS first: {embs_path}") safe_docs = _safe_open_path(str(docs_path), allowed_dir) - with open(safe_docs, "rb") as f: - docs = pickle.load(f) + docs = pickle.loads(safe_docs.read_bytes()) safe_table_idx = _safe_open_path(str(table_idx_path), allowed_dir) - with open(safe_table_idx, "rb") as f: - table_to_idx = pickle.load(f) + table_to_idx = pickle.loads(safe_table_idx.read_bytes()) embs = np.load(embs_path, mmap_mode="r")