|
23 | 23 | from flask_sqlalchemy.model import DefaultMeta |
24 | 24 | from sqlalchemy import func, delete, cast as sql_cast, literal, or_ |
25 | 25 | from sqlalchemy.dialects.postgresql import JSONB |
26 | | -from sqlalchemy.exc import OperationalError, IntegrityError |
| 26 | +from sqlalchemy.exc import OperationalError, IntegrityError, SQLAlchemyError |
27 | 27 |
|
28 | 28 | from neomodel import ( |
29 | 29 | config, |
@@ -2162,6 +2162,10 @@ def text_search(self, text: str) -> List[Optional[cre_defs.Document]]: |
2162 | 2162 | CRE ids before it performs a free text search |
2163 | 2163 | Anything else will be a case insensitive LIKE query in the database |
2164 | 2164 | """ |
| 2165 | + search_terms = self._expand_text_search_aliases(text) |
| 2166 | + if not search_terms: |
| 2167 | + return [] |
| 2168 | + text = text.strip() |
2165 | 2169 | # structured text search first |
2166 | 2170 | cre_id_search = r"CRE(:| )(?P<id>\d+-\d+)" |
2167 | 2171 | cre_naked_id_search = r"\d\d\d-\d\d\d" |
@@ -2213,34 +2217,81 @@ def text_search(self, text: str) -> List[Optional[cre_defs.Document]]: |
2213 | 2217 | if results: |
2214 | 2218 | return list(set(results)) |
2215 | 2219 | # fuzzy matches second |
2216 | | - args = [f"%{text}%", "", "", "", "", ""] |
2217 | 2220 | results = {} |
2218 | | - s = set([p for p in permutations(args, 6)]) |
2219 | | - for combo in s: |
2220 | | - nodes = self.get_nodes( |
2221 | | - name=combo[0], |
2222 | | - section=combo[1], |
2223 | | - subsection=combo[2], |
2224 | | - link=combo[3], |
2225 | | - description=combo[4], |
2226 | | - partial=True, |
2227 | | - ntype=None, # type: ignore |
2228 | | - sectionID=combo[5], |
2229 | | - ) |
2230 | | - if nodes: |
2231 | | - for node in nodes: |
2232 | | - node_key = f"{node.name}:{node.version}:{node.section}:{node.sectionID}:{node.subsection}:" |
2233 | | - results[node_key] = node |
2234 | | - args = [f"%{text}%", None, None] |
2235 | | - for combo in permutations(args, 3): |
2236 | | - cres = self.get_CREs( |
2237 | | - name=combo[0], external_id=combo[1], description=combo[2], partial=True |
2238 | | - ) |
2239 | | - if cres: |
2240 | | - for cre in cres: |
2241 | | - results[cre.id] = cre |
| 2221 | + for search_term in search_terms: |
| 2222 | + args = [f"%{search_term}%", "", "", "", "", ""] |
| 2223 | + s = set([p for p in permutations(args, 6)]) |
| 2224 | + for combo in s: |
| 2225 | + nodes = self.get_nodes( |
| 2226 | + name=combo[0], |
| 2227 | + section=combo[1], |
| 2228 | + subsection=combo[2], |
| 2229 | + link=combo[3], |
| 2230 | + description=combo[4], |
| 2231 | + partial=True, |
| 2232 | + ntype=None, # type: ignore |
| 2233 | + sectionID=combo[5], |
| 2234 | + ) |
| 2235 | + if nodes: |
| 2236 | + for node in nodes: |
| 2237 | + node_key = f"{node.name}:{node.version}:{node.section}:{node.sectionID}:{node.subsection}:" |
| 2238 | + results[node_key] = node |
| 2239 | + args = [f"%{search_term}%", None, None] |
| 2240 | + for combo in permutations(args, 3): |
| 2241 | + cres = self.get_CREs( |
| 2242 | + name=combo[0], |
| 2243 | + external_id=combo[1], |
| 2244 | + description=combo[2], |
| 2245 | + partial=True, |
| 2246 | + ) |
| 2247 | + if cres: |
| 2248 | + for cre in cres: |
| 2249 | + results[cre.id] = cre |
2242 | 2250 | return list(results.values()) |
2243 | 2251 |
|
| 2252 | + def _expand_text_search_aliases(self, text: str) -> List[str]: |
| 2253 | + normalized = text.strip() |
| 2254 | + if not normalized: |
| 2255 | + return [] |
| 2256 | + lowered = normalized.lower() |
| 2257 | + aliases = { |
| 2258 | + "xxe": [ |
| 2259 | + "XXE", |
| 2260 | + "XML External Entity", |
| 2261 | + "XML External Entity Reference", |
| 2262 | + ], |
| 2263 | + "xss": [ |
| 2264 | + "XSS", |
| 2265 | + "Cross Site Scripting", |
| 2266 | + "Cross-Site Scripting", |
| 2267 | + ], |
| 2268 | + "ssrf": [ |
| 2269 | + "SSRF", |
| 2270 | + "Server Side Request Forgery", |
| 2271 | + "Server-Side Request Forgery", |
| 2272 | + ], |
| 2273 | + "csrf": [ |
| 2274 | + "CSRF", |
| 2275 | + "Cross Site Request Forgery", |
| 2276 | + "Cross-Site Request Forgery", |
| 2277 | + ], |
| 2278 | + "rce": [ |
| 2279 | + "RCE", |
| 2280 | + "Remote Code Execution", |
| 2281 | + ], |
| 2282 | + } |
| 2283 | + expanded = [normalized] |
| 2284 | + expanded.extend(aliases.get(lowered, [])) |
| 2285 | + deduped = [] |
| 2286 | + seen = set() |
| 2287 | + for entry in expanded: |
| 2288 | + key = entry.lower() |
| 2289 | + if key in seen: |
| 2290 | + continue |
| 2291 | + seen.add(key) |
| 2292 | + deduped.append(entry) |
| 2293 | + return deduped |
| 2294 | + |
2244 | 2295 | def get_root_cres(self): |
2245 | 2296 | """Returns CRES that only have "Contains" links""" |
2246 | 2297 | # select name from cre where cre.id not in (select cre from cre_links where type="Contains") and cre.id not in (select "group" from cre_links where type="Is Part OF"); |
@@ -2289,7 +2340,7 @@ def health_check(self) -> Dict[str, Any]: |
2289 | 2340 | "db_reachable": False, |
2290 | 2341 | "reason": "database unreachable", |
2291 | 2342 | } |
2292 | | - except Exception: # pragma: no cover - defensive, never fail open |
| 2343 | + except SQLAlchemyError: # pragma: no cover - defensive, never fail open |
2293 | 2344 | return { |
2294 | 2345 | "ok": False, |
2295 | 2346 | "db_reachable": False, |
|
0 commit comments