dorksmith 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {dorksmith-0.1.0 → dorksmith-0.2.0}/.gitignore +3 -0
  2. {dorksmith-0.1.0 → dorksmith-0.2.0}/PKG-INFO +5 -5
  3. {dorksmith-0.1.0 → dorksmith-0.2.0}/README.md +4 -4
  4. {dorksmith-0.1.0 → dorksmith-0.2.0}/pyproject.toml +1 -1
  5. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/__init__.py +1 -1
  6. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/normalizer.py +24 -2
  7. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/quoting.py +8 -2
  8. {dorksmith-0.1.0 → dorksmith-0.2.0}/tests/test_golden.py +1 -1
  9. dorksmith-0.2.0/tests/test_phrases.py +42 -0
  10. {dorksmith-0.1.0 → dorksmith-0.2.0}/LICENSE +0 -0
  11. {dorksmith-0.1.0 → dorksmith-0.2.0}/scripts/sync_catalogs.py +0 -0
  12. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/analyzer.py +0 -0
  13. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/catalogs.py +0 -0
  14. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/cli.py +0 -0
  15. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/data/filetypes.json +0 -0
  16. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/data/intents.json +0 -0
  17. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/data/operators.google.json +0 -0
  18. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/data/platforms.json +0 -0
  19. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/errors.py +0 -0
  20. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/generator.py +0 -0
  21. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/handles.py +0 -0
  22. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/infer.py +0 -0
  23. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/placeholders.py +0 -0
  24. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/py.typed +0 -0
  25. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/ranker.py +0 -0
  26. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/resolver.py +0 -0
  27. {dorksmith-0.1.0 → dorksmith-0.2.0}/src/dorksmith/validator.py +0 -0
  28. {dorksmith-0.1.0 → dorksmith-0.2.0}/tests/test_unit.py +0 -0
@@ -53,3 +53,6 @@ __pycache__/
53
53
  *.pyc
54
54
  .pytest_cache/
55
55
  .venv/
56
+
57
+ # Generated engine copy for the SPA (npm run build)
58
+ web/js/engine/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dorksmith
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Deterministic search-dork generator and OSINT/SOCMINT query engine. Catalog-driven, pure Python, no dependencies.
5
5
  Project-URL: Homepage, https://github.com/aelena/dorksmith
6
6
  Project-URL: Repository, https://github.com/aelena/dorksmith
@@ -78,7 +78,7 @@ validate_query("cache:example.com site:example.com or filetype:.pdf").warnings
78
78
  # ["Lower-case 'or' is treated as an ordinary word; ...", "filetype: values take no leading dot ...", "cache: is deprecated: ..."]
79
79
  ```
80
80
 
81
- Results are dataclasses; `.to_dict()` gives the same camelCase shape as the HTTP API (`POST /api/v1/dorks/generate`), minus `requestId` and `rateLimit`.
81
+ Results are dataclasses; `.to_dict()` gives the same camelCase shape the npm package and the web app use.
82
82
 
83
83
  ## API
84
84
 
@@ -89,11 +89,11 @@ Results are dataclasses; `.to_dict()` gives the same camelCase shape as the HTTP
89
89
  | `expand_handle(username, *, categories=None, platform_ids=None, max_platforms=None, catalogs=None)` | Profile URLs (`status` always `not-checked`) and site-scoped queries. |
90
90
  | `infer_input_type(text, known_extensions=None)` | Deterministic input-type suggestion. |
91
91
  | `search_url(query, engine="google")` | Search-engine URL, percent-encoded locally. |
92
- | `validate_catalogs(catalogs)` | Structural/cross-reference validation, same rules as the API's readiness check. |
92
+ | `validate_catalogs(catalogs)` | Structural/cross-reference validation of the catalogs. |
93
93
  | `bundled_catalogs()`, `load_catalogs(path)`, `catalog_version()` | Embedded or custom catalogs. |
94
94
  | lower-level: `normalize_text`, `try_normalize_domain`, `quote`, `safe_term`, `analyze_query`, `expand_template`, … | Building blocks for custom pipelines. |
95
95
 
96
- `options` keys match the HTTP API: `fileTypes`, `excludeTerms`, `after`, `before`, `site`, `maxVariants`, `organization`, `location`, `role`, `displayName`.
96
+ `options` keys match the npm package: `fileTypes`, `excludeTerms`, `after`, `before`, `site`, `maxVariants`, `organization`, `location`, `role`, `displayName`.
97
97
 
98
98
  ### Bring your own catalogs
99
99
 
@@ -121,7 +121,7 @@ Exit codes: `0` ok, `2` invalid input (or a query with syntax errors for `valida
121
121
  ```bash
122
122
  python -m pip install -e ".[test]"
123
123
  python scripts/sync_catalogs.py # copies ../../data/*.json into src/dorksmith/data
124
- python -m pytest -q # unit tests + the 151 golden fixtures from the monorepo
124
+ python -m pytest -q # unit tests + the 153 golden fixtures from the monorepo
125
125
  python -m build # sdist + wheel
126
126
  ```
127
127
 
@@ -28,7 +28,7 @@ validate_query("cache:example.com site:example.com or filetype:.pdf").warnings
28
28
  # ["Lower-case 'or' is treated as an ordinary word; ...", "filetype: values take no leading dot ...", "cache: is deprecated: ..."]
29
29
  ```
30
30
 
31
- Results are dataclasses; `.to_dict()` gives the same camelCase shape as the HTTP API (`POST /api/v1/dorks/generate`), minus `requestId` and `rateLimit`.
31
+ Results are dataclasses; `.to_dict()` gives the same camelCase shape the npm package and the web app use.
32
32
 
33
33
  ## API
34
34
 
@@ -39,11 +39,11 @@ Results are dataclasses; `.to_dict()` gives the same camelCase shape as the HTTP
39
39
  | `expand_handle(username, *, categories=None, platform_ids=None, max_platforms=None, catalogs=None)` | Profile URLs (`status` always `not-checked`) and site-scoped queries. |
40
40
  | `infer_input_type(text, known_extensions=None)` | Deterministic input-type suggestion. |
41
41
  | `search_url(query, engine="google")` | Search-engine URL, percent-encoded locally. |
42
- | `validate_catalogs(catalogs)` | Structural/cross-reference validation, same rules as the API's readiness check. |
42
+ | `validate_catalogs(catalogs)` | Structural/cross-reference validation of the catalogs. |
43
43
  | `bundled_catalogs()`, `load_catalogs(path)`, `catalog_version()` | Embedded or custom catalogs. |
44
44
  | lower-level: `normalize_text`, `try_normalize_domain`, `quote`, `safe_term`, `analyze_query`, `expand_template`, … | Building blocks for custom pipelines. |
45
45
 
46
- `options` keys match the HTTP API: `fileTypes`, `excludeTerms`, `after`, `before`, `site`, `maxVariants`, `organization`, `location`, `role`, `displayName`.
46
+ `options` keys match the npm package: `fileTypes`, `excludeTerms`, `after`, `before`, `site`, `maxVariants`, `organization`, `location`, `role`, `displayName`.
47
47
 
48
48
  ### Bring your own catalogs
49
49
 
@@ -71,7 +71,7 @@ Exit codes: `0` ok, `2` invalid input (or a query with syntax errors for `valida
71
71
  ```bash
72
72
  python -m pip install -e ".[test]"
73
73
  python scripts/sync_catalogs.py # copies ../../data/*.json into src/dorksmith/data
74
- python -m pytest -q # unit tests + the 151 golden fixtures from the monorepo
74
+ python -m pytest -q # unit tests + the 153 golden fixtures from the monorepo
75
75
  python -m build # sdist + wheel
76
76
  ```
77
77
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dorksmith"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Deterministic search-dork generator and OSINT/SOCMINT query engine. Catalog-driven, pure Python, no dependencies."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -29,7 +29,7 @@ from .placeholders import ALL_PLACEHOLDERS, OPTIONAL_BY_DEFAULT, placeholders_in
29
29
  from .quoting import exclusion, looks_like_syntax, operator_value, or_group, quote, safe_term, safe_terms, unquote
30
30
  from .validator import validate_catalogs
31
31
 
32
- __version__ = "0.1.0"
32
+ __version__ = "0.2.0"
33
33
 
34
34
 
35
35
  def catalog_version(catalogs: Catalogs | None = None) -> str:
@@ -51,12 +51,34 @@ def normalize_text(value: str | None) -> str:
51
51
 
52
52
 
53
53
  def words(normalized: str) -> tuple[list[str], bool]:
54
- """Split normalised text into words, unwrapping a fully quoted phrase ("a b" -> a b)."""
54
+ """Split normalised text into terms.
55
+
56
+ A fully quoted input ("a b") is unwrapped; quoted phrases inside the text (alice "antonio elena")
57
+ are kept as single terms, quotes included, so templates preserve them.
58
+ """
55
59
  s = normalized
56
60
  quoted = len(s) >= 2 and s[0] == '"' and s[-1] == '"' and s.find('"', 1) == len(s) - 1
57
61
  if quoted:
58
62
  s = s[1:-1].strip()
59
- return [w for w in (p.strip() for p in s.split(" ")) if w], quoted
63
+ out: list[str] = []
64
+ i, n = 0, len(s)
65
+ while i < n:
66
+ if s[i] == " ":
67
+ i += 1
68
+ continue
69
+ if s[i] == '"':
70
+ end = s.find('"', i + 1)
71
+ inner = (s[i + 1:] if end < 0 else s[i + 1:end]).strip()
72
+ if inner:
73
+ out.append(f'"{inner}"')
74
+ i = n if end < 0 else end + 1
75
+ continue
76
+ j = i
77
+ while j < n and s[j] != " ":
78
+ j += 1
79
+ out.append(s[i:j])
80
+ i = j
81
+ return out, quoted
60
82
 
61
83
 
62
84
  def try_normalize_domain(value: str | None) -> str | None:
@@ -27,6 +27,8 @@ def looks_like_syntax(s: str) -> bool:
27
27
  return True
28
28
  if s[0] in "-+~()|":
29
29
  return True
30
+ if len(s) > 1 and s[0] in "@#": # social-handle / hashtag operators
31
+ return True
30
32
  if s[-1] in "()":
31
33
  return True
32
34
  colon = s.find(":")
@@ -36,8 +38,12 @@ def looks_like_syntax(s: str) -> bool:
36
38
 
37
39
 
38
40
  def safe_term(word: str | None) -> str:
39
- """A single word emitted unquoted; words that would parse as syntax are quoted so they stay literal."""
40
- s = (word or "").strip().replace('"', "")
41
+ """A single term emitted unquoted; terms that would parse as syntax are quoted so they stay literal,
42
+ and a term the user already quoted ("antonio elena") is kept as a quoted phrase."""
43
+ raw = (word or "").strip()
44
+ if len(raw) >= 2 and raw[0] == '"' and raw[-1] == '"':
45
+ return quote(raw)
46
+ s = raw.replace('"', "")
41
47
  if not s:
42
48
  return ""
43
49
  if looks_like_syntax(s):
@@ -11,7 +11,7 @@ import pytest
11
11
 
12
12
  from dorksmith import bundled_catalogs, generate, validate_catalogs
13
13
 
14
- FIXTURES = Path(__file__).resolve().parents[3] / "tests" / "Dorksmith.Api.Tests" / "Golden" / "fixtures"
14
+ FIXTURES = Path(__file__).resolve().parents[3] / "tests" / "golden"
15
15
  FILES = sorted(FIXTURES.glob("*.json")) if FIXTURES.is_dir() else []
16
16
  _covered: set[str] = set()
17
17
 
@@ -0,0 +1,42 @@
1
+ """Quoted phrases and social-style tokens inside free-text input."""
2
+ from __future__ import annotations
3
+
4
+ from dorksmith import analyze_query, bundled_catalogs, generate, safe_term
5
+ from dorksmith.normalizer import words
6
+
7
+
8
+ def test_words_keeps_user_quoted_phrases() -> None:
9
+ assert words('aelena "antonio elena"')[0] == ["aelena", '"antonio elena"']
10
+ assert words('"antonio elena" aelena x')[0] == ['"antonio elena"', "aelena", "x"]
11
+ assert words('a "" b')[0] == ["a", "b"]
12
+ assert words('a "unterminated phrase')[0] == ["a", '"unterminated phrase"']
13
+ assert words('"whole input quoted"') == (["whole", "input", "quoted"], True)
14
+
15
+
16
+ def test_safe_term_keeps_phrases_and_neutralises_social_tokens() -> None:
17
+ assert safe_term('"antonio elena"') == '"antonio elena"'
18
+ assert safe_term('"nested "quotes" inside"') == '"nested quotes inside"'
19
+ assert safe_term('""') == ""
20
+ assert safe_term("@aelena") == '"@aelena"'
21
+ assert safe_term("#osint") == '"#osint"'
22
+ assert safe_term("@") == "@"
23
+ assert safe_term("a@b") == "a@b"
24
+
25
+
26
+ def test_quoted_phrase_survives_variants() -> None:
27
+ r = generate('aelena "antonio elena"', "keyword", "general-discovery", options={"maxVariants": 12})
28
+ queries = [v.query for v in r.variants]
29
+ assert 'aelena "antonio elena"' in queries
30
+ assert '(aelena OR "antonio elena")' in queries
31
+ assert '"aelena antonio elena"' in queries
32
+ assert all('""' not in q for q in queries)
33
+
34
+
35
+ def test_leading_at_is_quoted_without_operator_warning() -> None:
36
+ r = generate("@aelena antonio elena", "keyword", "general-discovery", options={"maxVariants": 12})
37
+ for v in r.variants:
38
+ assert "@" not in v.operators, v.query
39
+ assert v.warnings == [], v.query
40
+ assert any(v.query == '"@aelena" antonio elena' for v in r.variants)
41
+ catalogs = bundled_catalogs()
42
+ assert analyze_query('"@aelena" antonio elena', catalogs.operators["google"], catalogs.operator_index("google")).operators == ['"']
File without changes
File without changes