osintkit 0.1.1__py3-none-any.whl

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.
osintkit/synthesis.py ADDED
@@ -0,0 +1,176 @@
1
+ """Prompt construction, model call, strict validation, and the single repair retry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from typing import Any
8
+
9
+ from .errors import SourceUnavailable, SynthesisError
10
+ from .llm import LLMClient
11
+ from .models import Analysis, PromptBundle
12
+
13
+ SYSTEM_PROMPT = """\
14
+ You are a defensive OSINT analyst. You receive raw results gathered from public sources \
15
+ about a single target. Work only from the supplied data plus well-known public facts about \
16
+ the platforms named in it. Never invent a breach, an account, or a hostname.
17
+
18
+ Produce a compact, source-grounded briefing for someone doing a personal privacy check or \
19
+ authorised research.
20
+
21
+ Rules:
22
+ - Cite the supplied source name in every breach entry. Mark dates as found; use null when unknown.
23
+ - If a source reports more breaches than it listed, say so and keep the 15 most material, ordering
24
+ credential-bearing and recent incidents first.
25
+ - Distinguish confirmed findings from plausible inference in the summary and notes.
26
+ - Risk level reflects exposure only: high means credential-bearing breaches or a wide,
27
+ live attack surface.
28
+ - Recommendations must be concrete and actionable, not generic advice.
29
+ - If a source failed or was skipped, note the gap in infrastructure_notes.
30
+
31
+ Return ONLY a JSON object, no prose and no code fences, with exactly these keys:
32
+ {
33
+ "summary": "3-6 sentence overview of the public footprint",
34
+ "breaches": [{"name": "str", "date": "YYYY-MM-DD or null", "data_types": ["str"], "source": "str"}],
35
+ "associated_accounts": [
36
+ {"platform": "str", "url": "str or null", "handle": "str or null", "note": "str or null"}
37
+ ],
38
+ "infrastructure_notes": ["str"],
39
+ "subdomains": ["str"],
40
+ "risk_level": "low" | "medium" | "high",
41
+ "recommendations": ["str"]
42
+ }
43
+ Use empty arrays when a section has no findings.\
44
+ """
45
+
46
+ USER_TEMPLATE = """\
47
+ Target: {target}
48
+ Target type: {target_type}
49
+
50
+ Raw public-source data:
51
+ ```json
52
+ {payload}
53
+ ```
54
+
55
+ Write the briefing now as strict JSON.\
56
+ """
57
+
58
+ REPAIR_PROMPT = """\
59
+ That response was not valid against the required schema. Error: {error}
60
+
61
+ Return the corrected JSON object only. All seven keys are required, arrays instead of prose \
62
+ for every list field, and risk_level must be exactly one of "low", "medium", or "high".\
63
+ """
64
+
65
+
66
+ def build_messages(bundle: PromptBundle) -> list[dict[str, str]]:
67
+ return [
68
+ {"role": "system", "content": SYSTEM_PROMPT},
69
+ {
70
+ "role": "user",
71
+ "content": USER_TEMPLATE.format(
72
+ target=bundle.target,
73
+ target_type=bundle.target_type,
74
+ payload=bundle.to_prompt_json(),
75
+ ),
76
+ },
77
+ ]
78
+
79
+
80
+ def render_prompt(bundle: PromptBundle) -> str:
81
+ parts = []
82
+ for message in build_messages(bundle):
83
+ parts.append(f"### {message['role']}\n\n{message['content']}")
84
+ return "\n\n".join(parts)
85
+
86
+
87
+ def extract_json(text: str) -> dict[str, Any]:
88
+ cleaned = text.strip()
89
+ if cleaned.startswith("```"):
90
+ cleaned = cleaned.split("\n", 1)[-1]
91
+ if cleaned.rstrip().endswith("```"):
92
+ cleaned = cleaned.rstrip()[:-3]
93
+ start = cleaned.find("{")
94
+ end = cleaned.rfind("}")
95
+ if start == -1 or end == -1 or end <= start:
96
+ raise SynthesisError("no JSON object in model output")
97
+ blob = cleaned[start : end + 1]
98
+ try:
99
+ parsed = json.loads(blob)
100
+ except json.JSONDecodeError as exc:
101
+ raise SynthesisError(f"invalid JSON: {exc.msg} at offset {exc.pos}") from exc
102
+ if not isinstance(parsed, dict):
103
+ raise SynthesisError("model output was JSON but not an object")
104
+ return parsed
105
+
106
+
107
+ async def synthesize(
108
+ client: LLMClient,
109
+ bundle: PromptBundle,
110
+ *,
111
+ logger: Any | None = None,
112
+ max_tokens: int = 2400,
113
+ ) -> Analysis:
114
+ messages = build_messages(bundle)
115
+
116
+ settings = getattr(client, "settings", None)
117
+
118
+ async def call(current: list[dict[str, str]], attempt: int) -> str:
119
+ if logger is not None:
120
+ logger.request(
121
+ base_url=getattr(settings, "base_url", "unknown"),
122
+ model=getattr(settings, "model", "unknown"),
123
+ attempt=attempt,
124
+ dry_run=False,
125
+ messages=current,
126
+ max_tokens=max_tokens,
127
+ )
128
+ started = time.perf_counter()
129
+ try:
130
+ try:
131
+ content = await client.chat(
132
+ current,
133
+ max_tokens=max_tokens,
134
+ response_format={"type": "json_object"},
135
+ )
136
+ except SourceUnavailable as exc:
137
+ if "HTTP 400" not in str(exc):
138
+ raise
139
+ content = await client.chat(current, max_tokens=max_tokens)
140
+ except Exception as exc:
141
+ if logger is not None:
142
+ logger.response(
143
+ attempt=attempt,
144
+ ok=False,
145
+ error=str(exc),
146
+ elapsed_ms=int((time.perf_counter() - started) * 1000),
147
+ content=None,
148
+ )
149
+ raise
150
+ if logger is not None:
151
+ logger.response(
152
+ attempt=attempt,
153
+ ok=True,
154
+ error=None,
155
+ elapsed_ms=int((time.perf_counter() - started) * 1000),
156
+ content=content,
157
+ )
158
+ return content
159
+
160
+ raw = ""
161
+ try:
162
+ raw = await call(messages, 1)
163
+ return Analysis.model_validate(extract_json(raw))
164
+ except (SynthesisError, ValueError) as first_error:
165
+ repair = [
166
+ *messages,
167
+ {"role": "assistant", "content": raw or "(empty)"},
168
+ {"role": "user", "content": REPAIR_PROMPT.format(error=str(first_error)[:400])},
169
+ ]
170
+ raw_retry = await call(repair, 2)
171
+ try:
172
+ return Analysis.model_validate(extract_json(raw_retry))
173
+ except (SynthesisError, ValueError) as second_error:
174
+ raise SynthesisError(
175
+ f"model output still invalid after one repair attempt: {second_error}"
176
+ ) from second_error
osintkit/targets.py ADDED
@@ -0,0 +1,65 @@
1
+ """Target typing and normalisation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from .errors import TargetParseError
8
+
9
+ EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$")
10
+ DOMAIN_RE = re.compile(r"^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,24}$")
11
+ USERNAME_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,38})$")
12
+ COMPANY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 .,&'()+-]*$")
13
+ COMPANY_HINTS = re.compile(r"\b(inc|llc|ltd|gmbh|corp|corporation|limited|plc|ag|sa|bv|oy|pty)\b", re.I)
14
+
15
+ VALID_TYPES = ("email", "username", "domain", "company")
16
+
17
+
18
+ def normalise(target: str) -> str:
19
+ target = target.strip()
20
+ if target.lower().startswith("http://") or target.lower().startswith("https://"):
21
+ target = target.split("://", 1)[1]
22
+ target = target.strip().strip("<>\"'")
23
+ if "@" not in target:
24
+ target = target.split("/", 1)[0]
25
+ if target.lower().startswith("www."):
26
+ target = target[4:]
27
+ return target.rstrip("/")
28
+
29
+
30
+ def detect_type(target: str) -> str:
31
+ if EMAIL_RE.match(target):
32
+ return "email"
33
+ if DOMAIN_RE.match(target):
34
+ return "domain"
35
+ if " " in target or COMPANY_HINTS.search(target):
36
+ return "company"
37
+ if USERNAME_RE.match(target):
38
+ return "username"
39
+ raise TargetParseError(f"could not tell what {target!r} is; pass --type explicitly")
40
+
41
+
42
+ def resolve(target: str, declared: str | None = None) -> tuple[str, str]:
43
+ clean = normalise(target)
44
+ if not clean:
45
+ raise TargetParseError("empty target")
46
+ kind = (declared or "").lower() or None
47
+ if kind and kind not in VALID_TYPES:
48
+ raise TargetParseError(f"unknown type {kind!r}; expected one of {', '.join(VALID_TYPES)}")
49
+ if kind:
50
+ _check_shape(clean, kind)
51
+ return clean, kind
52
+ kind = detect_type(clean)
53
+ _check_shape(clean, kind)
54
+ return clean, kind
55
+
56
+
57
+ def _check_shape(target: str, kind: str) -> None:
58
+ if kind == "email" and not EMAIL_RE.match(target):
59
+ raise TargetParseError(f"{target!r} is not a valid email")
60
+ if kind == "domain" and not DOMAIN_RE.match(target):
61
+ raise TargetParseError(f"{target!r} is not a valid domain")
62
+ if kind == "username" and not USERNAME_RE.match(target):
63
+ raise TargetParseError(f"{target!r} is not a valid username")
64
+ if kind == "company" and not COMPANY_RE.match(target):
65
+ raise TargetParseError(f"{target!r} does not look like a company name")
@@ -0,0 +1,176 @@
1
+ Metadata-Version: 2.5
2
+ Name: osintkit
3
+ Version: 0.1.1
4
+ Summary: Public-data OSINT aggregator for emails, usernames, domains, and companies
5
+ Project-URL: Homepage, https://github.com/sjdsjadjajdjsa/osintkit
6
+ Project-URL: Issues, https://github.com/sjdsjadjajdjsa/osintkit/issues
7
+ Author: osintkit contributors
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: cli,osint,privacy,recon,security
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Security
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: dnspython>=2.6
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.7
23
+ Requires-Dist: python-whois>=0.9
24
+ Requires-Dist: rich>=13.7
25
+ Requires-Dist: typer>=0.12
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
29
+ Requires-Dist: pytest>=8.2; extra == 'dev'
30
+ Requires-Dist: respx>=0.21; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # osintkit
34
+
35
+ [![ci](https://github.com/sjdsjadjajdjsa/osintkit/actions/workflows/ci.yml/badge.svg)](https://github.com/sjdsjadjajdjsa/osintkit/actions/workflows/ci.yml)
36
+ [![python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue)](pyproject.toml)
37
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
38
+ [![ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
39
+
40
+ Public-data OSINT for one email, username, domain, or company: gathered from free sources,
41
+ briefed by an LLM acting as a defensive analyst, rendered as a colour-coded report.
42
+
43
+ `osintkit` uses only public sources: certificate transparency, public DNS, WHOIS, public GitHub
44
+ search, free breach indexes, paste archives, and public profile pages. It never logs in on your
45
+ behalf and never touches a paid data broker.
46
+
47
+ <img src="docs/demo.svg" alt="osintkit report showing risk, breaches, and recommendations" width="820">
48
+
49
+ A full trimmed report, including breach and account sections, is in
50
+ [docs/sample-output.md](docs/sample-output.md).
51
+
52
+ ## Why
53
+
54
+ Checking what the public record says about you is tedious: a dozen sites, a dozen formats, no
55
+ synthesis. `osintkit` runs those checks concurrently, keeps the raw evidence, and turns it into
56
+ a briefing with a risk level and concrete hardening advice. Everything is local, every source
57
+ is named, and every gap is reported instead of hidden.
58
+
59
+ Intended for personal privacy checks, authorised pre-engagement recon, and quick due diligence
60
+ on a domain or company. Findings are leads to verify, not facts.
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ uv tool install git+https://github.com/sjdsjadjajdjsa/osintkit
66
+ # or
67
+ pipx install git+https://github.com/sjdsjadjajdjsa/osintkit
68
+ ```
69
+
70
+ [Releases](https://github.com/sjdsjadjajdjsa/osintkit/releases) carry a signed-checksum wheel;
71
+ `pip install osintkit-0.1.0-py3-none-any.whl` works from one. From a clone,
72
+ `pip install -e .` also works. Python 3.12+ only.
73
+
74
+ ## Quick start
75
+
76
+ ```bash
77
+ export API_KEY=... # model key, any OpenAI-compatible endpoint
78
+ osintkit doctor --deep # prove the key and endpoint work
79
+ osintkit lookup alice@example.com
80
+ ```
81
+
82
+ No key? `osintkit lookup example.com --raw` still collects everything; `--dry-run` shows the
83
+ exact synthesis prompt.
84
+
85
+ Any OpenAI-compatible endpoint works. `--provider venice|openrouter|openai` picks a preset, and
86
+ a recognisable key prefix picks one for you; `--base-url` and `--model` always win.
87
+
88
+ Optional keys: `HIBP_API_KEY` turns on Have I Been Pwned, `GITHUB_TOKEN` lifts GitHub from 60 to
89
+ 5000 requests/hour. Everything else needs no credentials.
90
+
91
+ ## Commands
92
+
93
+ | Command | Purpose |
94
+ | --- | --- |
95
+ | `lookup` | One target, one report (`--email`, `--username`, `--domain`, `--company`) |
96
+ | `batch` | A CSV of targets, run concurrently, reported in order |
97
+ | `diff` | What changed between two archived runs |
98
+ | `history` | Past runs, replayable by id |
99
+ | `doctor` | Key, endpoint, and directory checks (`--deep` verifies auth) |
100
+ | `collectors` | Every collector and what it touches |
101
+ | `cache` / `prune` | Inspect, clear, or age out local state |
102
+ | `schema` | The JSON contract the model must satisfy |
103
+
104
+ Output: default colour report, `--json`, `--markdown` (they combine), `--raw` for collected
105
+ data, `--dry-run` for the prompt. Exit codes: `0` ok, `1` synthesis or partial failure, `2` bad
106
+ input.
107
+
108
+ ## How it works
109
+
110
+ ```
111
+ target -> type detection -> collectors (parallel, cached, retried)
112
+ |
113
+ v
114
+ merge + risk signals (deterministic)
115
+ |
116
+ v
117
+ LLM briefing -> pydantic validation -> one repair retry
118
+ |
119
+ v
120
+ colour report . JSON . Markdown . archive
121
+ ```
122
+
123
+ Collectors degrade to a logged gap, never a dead run; a report is produced even when sources or
124
+ the model fail.
125
+
126
+ - **Merged breaches.** Three indexes listing one incident collapse into one row with every
127
+ source and the earliest date.
128
+ - **Risk floor.** Breach count and credential types set a floor the model may exceed but never
129
+ undercut.
130
+ - **Cached everything.** 24h default, shorter for volatile sources; repeat prompts cost nothing.
131
+ - **Validated output.** Strict JSON schema, one repair round-trip, then a clean failure.
132
+
133
+ ## Collectors
134
+
135
+ Two dozen sources across four target types, all free and login-free: HIBP (key), XposedOrNot,
136
+ LeakCheck, Gravatar, crt.sh (subdomains, certificates, organisation search), public DNS
137
+ (A/AAAA/MX/TXT/NS/CNAME plus SPF, DMARC, DKIM, wildcard), passive subdomains, WHOIS, TCP
138
+ connect probes, GitHub (profile, commits, user search, org repos), psbdmp with a Gist fallback,
139
+ grep.app, Hacker News, Stack Exchange, and username presence. The full table is in
140
+ [docs/REFERENCE.md](docs/REFERENCE.md#collectors) or via `osintkit collectors`.
141
+
142
+ ## Safety and legality
143
+
144
+ - Public data only. No logins, no scraping behind auth, no paid brokers.
145
+ - Targets are typed and normalised before any request.
146
+ - Requests retry on `429`/`5xx` with backoff and honour GitHub quota headers.
147
+ - Results are evidence, not ground truth; the report says which sources were unavailable.
148
+ - Cache, logs, and reports stay under `~/.osintkit` (override with `OSINTKIT_HOME`). Retention
149
+ is 30 days by default.
150
+
151
+ ## Limitations
152
+
153
+ WHOIS is often redacted. GitHub search is shallow. Free breach indexes lag. Port probing only
154
+ sees what your network can reach. Model summaries can overstate weak signals. Read the coverage
155
+ panel: an empty section may mean "nothing found" or "source skipped".
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ git clone https://github.com/sjdsjadjajdjsa/osintkit
161
+ cd osintkit
162
+ uv sync --extra dev
163
+ pytest
164
+ uvx ruff check src tests
165
+ ```
166
+
167
+ Tests never touch the network: HTTP is mocked with `respx`, DNS and WHOIS are monkeypatched, and
168
+ a recorded model reply covers the full-stack path. CI runs on 3.12 and 3.13. See
169
+ [CONTRIBUTING.md](CONTRIBUTING.md) for the collector contract and
170
+ [docs/architecture.md](docs/architecture.md) for the module map.
171
+
172
+ ## Licence
173
+
174
+ MIT. Security reports: [SECURITY.md](SECURITY.md).
175
+
176
+ `osintkit` is independent tooling. It is not affiliated with any source it queries.
@@ -0,0 +1,33 @@
1
+ osintkit/__init__.py,sha256=nzOmu0EQ5zD3bXHXAG09l7UM6UN-hO_5H9kFS-vByGg,71
2
+ osintkit/cli.py,sha256=IOn7YGApgP6Dt1oO-U8Jc_m9Hmm99dgkDAoxlZw3ZkM,25214
3
+ osintkit/config.py,sha256=mYIazpKR4R1A75q4QiEWB00D4tjAQ3qWtpQxf94XvhM,4314
4
+ osintkit/diff.py,sha256=fY4Y-bEmqugUkZsul23zVwC3YBRchHqwphl0NuvpCRI,3536
5
+ osintkit/doctor.py,sha256=Fvyrg6Ke_5W6XYi8FAFwI9L1ReihE4dSPYtL409X52Y,4176
6
+ osintkit/errors.py,sha256=LpWYG6-BJeSOC7TZhJz_FxOMw_Nqwag3Odpm6nojeUw,1145
7
+ osintkit/http.py,sha256=wt0sWXpEpREJZrFaR--7OuEsyaCbFdENTw3fOmbm2AM,9461
8
+ osintkit/llm.py,sha256=jG_ubL_3_X324V46-Cu_0tZmSN2kAhuMFN92IdpoLdE,3237
9
+ osintkit/logging_store.py,sha256=erYmgS-TAqP7uXBhB2KCmMUWHLDR-7OcSOLXJrWGGWU,6226
10
+ osintkit/models.py,sha256=nRyg4rfRoU81koug53z5NuY-oX9uSlmgqAJejlP1JkI,7034
11
+ osintkit/normalise.py,sha256=eR_cSFHG4W_6zg5YqlbGizQF_ycrhBLd02lvFBPjjQM,4508
12
+ osintkit/pipeline.py,sha256=cKeI2vDf8F2GjOwbwFIReAZDATug_9iwThLODhnjv-Y,8072
13
+ osintkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ osintkit/render.py,sha256=hJB87iwVjy-AU_p8rRTtHpkGoOjrb_RpVqf0hGasy1U,8459
15
+ osintkit/retention.py,sha256=asCc5vNXxdyYLZ3V1LnlFzJxrOwDJYRdXkcFg4BkgLA,2332
16
+ osintkit/synthesis.py,sha256=ZXGLV7RYjC2W6oNimC2TOLeVdLueXrycNcTOi3eGL8E,6101
17
+ osintkit/targets.py,sha256=U7v5gobZmrWCF0nfKqBJkLO5U2o02_hckEcCZnP79wM,2447
18
+ osintkit/collectors/__init__.py,sha256=H1dbtafNPTa9YV0ox8VUZma8PLRM6vk8JOtH3XmYRuM,482
19
+ osintkit/collectors/accounts.py,sha256=gzLB5VBkHs7xReBpyfye12q1OBYxYAgr7gfFk33RRTU,2911
20
+ osintkit/collectors/base.py,sha256=aDJtrfpuxdjMtS1nsPho1lXa_IX0B5IUzftJvPpwJqY,3892
21
+ osintkit/collectors/breaches.py,sha256=HSwecmDdxuDkFVX33IUz6qTxvyetNVFZUCuGp452Vdk,5308
22
+ osintkit/collectors/ct.py,sha256=HJBe9z0qTeqASLyMUT4WbuOsPwShzufuuoYoR9585f8,5232
23
+ osintkit/collectors/dns.py,sha256=dceEKn01iUU6cd3FsJFBypb1uRYXmgw9Iihcid_d-BU,4519
24
+ osintkit/collectors/github.py,sha256=BGusnCVp9WnABst_apqv5uAs9aeVc15WESE4nO8Nz-8,6468
25
+ osintkit/collectors/pastes.py,sha256=0Zk3Qi3LG-iMTS2mSdStRuHSw7xN5sM0WzbZ0pexN7M,8810
26
+ osintkit/collectors/ports.py,sha256=pXsRPcF5Z-0tnM7wP_0hGpJdwRIAf0P7IekfflNHK_k,2700
27
+ osintkit/collectors/subdomains.py,sha256=i-osO4MyFAPcIrZ0blD3f8jIqauvPLaZ8QC36iaE_oo,3002
28
+ osintkit/collectors/whois_lookup.py,sha256=tkXalwMdEh4DQnVrECnF77ZrvBd5kFOh6xr-tVr2GMM,1859
29
+ osintkit-0.1.1.dist-info/METADATA,sha256=nFwcyLijcELGMC00PFTE8znwFRwZa-QJhFxya6bYx-4,7482
30
+ osintkit-0.1.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
31
+ osintkit-0.1.1.dist-info/entry_points.txt,sha256=_aW-Q6e3ikA8kWDSUI9EtCTwzECK00H5S8tWtLVkB4Q,46
32
+ osintkit-0.1.1.dist-info/licenses/LICENSE,sha256=RqsRrtEfRQOE5mURJPG7DJB1IUTa7XODM8FlMS7gUEM,1078
33
+ osintkit-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ osintkit = osintkit.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 osintkit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.