agentdata 0.3.26__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.
Files changed (120) hide show
  1. agentdata/__init__.py +33 -0
  2. agentdata/__main__.py +4 -0
  3. agentdata/cache.py +115 -0
  4. agentdata/cli.py +173 -0
  5. agentdata/client.py +158 -0
  6. agentdata/company/__init__.py +3 -0
  7. agentdata/company/cf.py +145 -0
  8. agentdata/company/ch_zefix.py +100 -0
  9. agentdata/company/cli.py +243 -0
  10. agentdata/company/co_rues.py +72 -0
  11. agentdata/company/edgar_cik.py +84 -0
  12. agentdata/company/edgar_fts.py +101 -0
  13. agentdata/company/faces.py +746 -0
  14. agentdata/company/fdic_bankfind.py +107 -0
  15. agentdata/company/fi_ytj.py +89 -0
  16. agentdata/company/fr_recherche.py +124 -0
  17. agentdata/company/fts.py +121 -0
  18. agentdata/company/gleif.py +178 -0
  19. agentdata/company/kr_hometax.py +112 -0
  20. agentdata/company/no_brreg.py +124 -0
  21. agentdata/company/ror.py +105 -0
  22. agentdata/company/ted.py +117 -0
  23. agentdata/company/usaspending.py +124 -0
  24. agentdata/company/vies.py +66 -0
  25. agentdata/daterange.py +35 -0
  26. agentdata/economy/__init__.py +10 -0
  27. agentdata/economy/bis.py +132 -0
  28. agentdata/economy/cli.py +199 -0
  29. agentdata/economy/ecb.py +117 -0
  30. agentdata/economy/eurostat.py +163 -0
  31. agentdata/economy/faces.py +573 -0
  32. agentdata/economy/fred.py +92 -0
  33. agentdata/economy/ilo_stat.py +99 -0
  34. agentdata/economy/imf_sdmx.py +133 -0
  35. agentdata/economy/un_comtrade.py +199 -0
  36. agentdata/economy/unsd_sdg.py +154 -0
  37. agentdata/economy/wb.py +208 -0
  38. agentdata/economy/who_xmart.py +177 -0
  39. agentdata/entries.py +30 -0
  40. agentdata/errors.py +38 -0
  41. agentdata/esg/__init__.py +5 -0
  42. agentdata/esg/cli.py +180 -0
  43. agentdata/esg/climatetrace.py +260 -0
  44. agentdata/esg/eiti.py +187 -0
  45. agentdata/esg/epa_frs.py +138 -0
  46. agentdata/esg/euasl.py +75 -0
  47. agentdata/esg/eutl.py +66 -0
  48. agentdata/esg/faces.py +498 -0
  49. agentdata/esg/paris_mou.py +240 -0
  50. agentdata/esg/thetis_mrv.py +67 -0
  51. agentdata/healthcare/__init__.py +7 -0
  52. agentdata/healthcare/au_tga.py +142 -0
  53. agentdata/healthcare/ca_dpd.py +123 -0
  54. agentdata/healthcare/ca_mdall.py +155 -0
  55. agentdata/healthcare/cli.py +375 -0
  56. agentdata/healthcare/ctgov.py +101 -0
  57. agentdata/healthcare/dailymed.py +102 -0
  58. agentdata/healthcare/epmc.py +108 -0
  59. agentdata/healthcare/eudamed.py +124 -0
  60. agentdata/healthcare/faces.py +811 -0
  61. agentdata/healthcare/fda_crl.py +86 -0
  62. agentdata/healthcare/fda_devices.py +199 -0
  63. agentdata/healthcare/fda_drugs.py +156 -0
  64. agentdata/healthcare/fda_recalls.py +137 -0
  65. agentdata/healthcare/gb_mhra.py +137 -0
  66. agentdata/healthcare/gudid.py +84 -0
  67. agentdata/healthcare/mesh.py +89 -0
  68. agentdata/healthcare/rasff.py +164 -0
  69. agentdata/healthcare/rxnav.py +67 -0
  70. agentdata/healthcare/safety_gate.py +175 -0
  71. agentdata/healthcare/shortages.py +107 -0
  72. agentdata/legal/__init__.py +4 -0
  73. agentdata/legal/afdb.py +93 -0
  74. agentdata/legal/ca_sema.py +143 -0
  75. agentdata/legal/cli.py +312 -0
  76. agentdata/legal/co_sic.py +70 -0
  77. agentdata/legal/courtlistener.py +77 -0
  78. agentdata/legal/csl.py +139 -0
  79. agentdata/legal/dgcomp.py +125 -0
  80. agentdata/legal/ecfr.py +129 -0
  81. agentdata/legal/faces.py +700 -0
  82. agentdata/legal/federalregister.py +188 -0
  83. agentdata/legal/gb_cma.py +117 -0
  84. agentdata/legal/gb_tna_caselaw.py +245 -0
  85. agentdata/legal/idb.py +85 -0
  86. agentdata/legal/lda.py +147 -0
  87. agentdata/legal/sipri.py +178 -0
  88. agentdata/legal/ukec.py +148 -0
  89. agentdata/legal/wb.py +92 -0
  90. agentdata/procurement/__init__.py +5 -0
  91. agentdata/procurement/br_pncp.py +126 -0
  92. agentdata/procurement/cli.py +155 -0
  93. agentdata/procurement/co_secop.py +88 -0
  94. agentdata/procurement/faces.py +239 -0
  95. agentdata/procurement/ny_open_data.py +82 -0
  96. agentdata/procurement/ungm.py +137 -0
  97. agentdata/procurement/wb_proc.py +84 -0
  98. agentdata/research/__init__.py +4 -0
  99. agentdata/research/biorxiv.py +66 -0
  100. agentdata/research/cli.py +129 -0
  101. agentdata/research/crossref.py +121 -0
  102. agentdata/research/faces.py +349 -0
  103. agentdata/research/gb_gtr.py +58 -0
  104. agentdata/research/icite.py +52 -0
  105. agentdata/research/nih_reporter.py +143 -0
  106. agentdata/research/nsf.py +68 -0
  107. agentdata/research/openalex.py +145 -0
  108. agentdata/research/opencitations.py +50 -0
  109. agentdata/security/__init__.py +3 -0
  110. agentdata/security/cisa_kev.py +42 -0
  111. agentdata/security/cli.py +37 -0
  112. agentdata/security/faces.py +91 -0
  113. agentdata/security/osv.py +72 -0
  114. agentdata/transport.py +541 -0
  115. agentdata-0.3.26.dist-info/METADATA +266 -0
  116. agentdata-0.3.26.dist-info/RECORD +120 -0
  117. agentdata-0.3.26.dist-info/WHEEL +5 -0
  118. agentdata-0.3.26.dist-info/entry_points.txt +10 -0
  119. agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
  120. agentdata-0.3.26.dist-info/top_level.txt +1 -0
@@ -0,0 +1,68 @@
1
+ """nsf — NSF Awards API (US NSF funding search; keyless).
2
+
3
+ No key needed (upstream states no keyless quota — self-imposed polite tier of 1 rps/burst 5).
4
+ Double response shell: the payload sits inside {"response": {"award": [...]}} (probed
5
+ 2026-10-26); keyword full-text search × id award-number point lookup — upstream actually
6
+ combines them with AND (measured 0.2.9), the local XOR is a sugar-split wall;
7
+ rpp 1-200 (upstream API doc wall, row-count re-verified 2026-10-28);
8
+ upstream sends no total — a full page signals more via has_more (len(rows)==rpp).
9
+ """
10
+ provider = {
11
+ "id": "nsf",
12
+ "baseUrl": "https://api.nsf.gov",
13
+ "rate": {"rps": 1, "burst": 5},
14
+ "ops": {
15
+ "awards": {"path": "/services/v1/awards.json", "method": "GET", "idempotent": True},
16
+ },
17
+ "ttl": {"awards": 24 * 3600},
18
+ }
19
+
20
+
21
+ def _award_row(a):
22
+ return {
23
+ "id": a.get("id"), "title": a.get("title"), "agency": a.get("agency"),
24
+ "date": a.get("date"), "start_date": a.get("startDate"), "exp_date": a.get("expDate"),
25
+ "amount": a.get("amount"), "principal_investigator": a.get("pi"),
26
+ "institution": a.get("institution"), "organization": a.get("organization"),
27
+ "fund_program": a.get("fundProgramName"), "cfda": a.get("cfda"),
28
+ "abstract_text": a.get("abstractText"), "award_notice_url": a.get("awardNoticeUrl"),
29
+ }
30
+
31
+
32
+ def awards_raw(keyword=None, *, id=None, rpp=25, offset=0, cache=None, no_cache=False):
33
+ """→ {total(=None, upstream sends no count), count, rows, has_more, params, _meta}; awards.json direct, verbatim name.
34
+ keyword XOR id (**a local wall, not upstream** — measured 2026-10-28 upstream combines with
35
+ AND: keyword=lithium&id=2624369→1 row, the intersection hit; sending both neither 400s nor
36
+ first-wins; the local XOR is a deliberate split between the full-text and point-lookup
37
+ channels); rpp 1-200 (upstream API doc; row-count re-verified 2026-10-28, rpp=200 truly
38
+ sends 200 rows); offset ≥0; an empty keyword semantically means browse-the-corpus (legal
39
+ upstream), no local wall here."""
40
+ from ..transport import call_provider
41
+ from ..errors import SdkError
42
+
43
+ kw = str(keyword or "").strip() or None
44
+ aid = str(id or "").strip() or None
45
+ if kw and aid:
46
+ raise SdkError("usage_error", provider="nsf",
47
+ detail="pass only one of keyword (full-text search) or id (award-number "
48
+ "point lookup) (local sugar-split wall — upstream actually combines "
49
+ "them with AND into the intersection, probed 2026-10-28)")
50
+ if not kw and not aid:
51
+ raise SdkError("usage_error", provider="nsf",
52
+ detail="nsf_awards requires at least one of keyword or id")
53
+ size = int(rpp)
54
+ if not 1 <= size <= 200:
55
+ raise SdkError("usage_error", provider="nsf",
56
+ detail="rpp must be 1-200 (upstream API doc page-size cap)")
57
+ off = int(offset)
58
+ if off < 0:
59
+ raise SdkError("usage_error", provider="nsf", detail="offset must be ≥0")
60
+ params = {"rpp": size, "offset": off}
61
+ if kw:
62
+ params["keyword"] = kw
63
+ else:
64
+ params["id"] = aid
65
+ r = call_provider(provider, "awards", params, cache=cache, no_cache=no_cache)
66
+ rows = [_award_row(a) for a in ((r["data"] or {}).get("response") or {}).get("award") or []]
67
+ return {"total": None, "count": len(rows), "rows": rows,
68
+ "has_more": len(rows) == size, "params": params, "_meta": r}
@@ -0,0 +1,145 @@
1
+ """openalex — OpenAlex API (global scholarly output and institution profiles; keyless).
2
+
3
+ No key needed (official polite pool 10 rps, self-imposed polite tier of 5 rps). Two faces
4
+ (probed 2026-10-26): works = paper rows (filter=institutions.ror: reaches the institution
5
+ edge; rows link back through authorships[].institutions[].ror); institutions = institution
6
+ candidates (search= is a fuzzy name search, same-name clones in different countries coexist —
7
+ the candidate list is never auto-picked, pick the ROR explicitly and then query output on the
8
+ works face, adversarial-review constraint 1).
9
+ Three traps: per-page>200 is an explicit 400 (an honest wall, enforced locally); select must
10
+ force-merge the core fields (stripping them drops the edges, adversarial-review constraint 2);
11
+ the cursor (meta.next_cursor) encodes paging only, not the conditions — when continuing you
12
+ must re-send the constraint parameters unchanged (adversarial-review constraint 3; this
13
+ wrapper satisfies it naturally by requiring at least one constraint).
14
+ """
15
+ provider = {
16
+ "id": "openalex",
17
+ "baseUrl": "https://api.openalex.org",
18
+ "rate": {"rps": 5, "burst": 5}, # quota semantics 5+5=10 ≤ the official 10 req/s (with burst 10 the 10+5=15 would exceed the cap, fixed on re-check in 0.2.5)
19
+ "ops": {
20
+ "works": {"path": "/works", "method": "GET", "idempotent": True},
21
+ "institutions": {"path": "/institutions", "method": "GET", "idempotent": True},
22
+ },
23
+ "ttl": {"works": 24 * 3600, "institutions": 24 * 3600},
24
+ }
25
+
26
+ _PER_PAGE_CAP = 200 # per-page>200 gets an explicit upstream 400 (probe 2026-10-26)
27
+ _CORE_FIELDS = "id,doi,display_name,publication_year,type,cited_by_count,open_access,authorships"
28
+
29
+
30
+ def _norm_ror(ror):
31
+ """ROR normalization: both the bare 15-char form (01xdqrp08) and the full URL
32
+ (https://ror.org/01xdqrp08) are accepted, both normalize to the full URL."""
33
+ v = str(ror or "").strip()
34
+ if not v:
35
+ return None
36
+ if v.startswith("https://ror.org/"):
37
+ return v
38
+ if v.startswith("ror.org/"):
39
+ return "https://" + v
40
+ return f"https://ror.org/{v}"
41
+
42
+
43
+ def _work_row(w):
44
+ return {
45
+ "id": w.get("id"), "doi": w.get("doi"), "display_name": w.get("display_name"),
46
+ "publication_year": w.get("publication_year"), "type": w.get("type"),
47
+ "cited_by_count": w.get("cited_by_count"),
48
+ "open_access": w.get("open_access") or {},
49
+ "authorships": [{"position": a.get("author_position"),
50
+ "author": (a.get("author") or {}).get("display_name"),
51
+ "raw_author_name": a.get("raw_author_name"),
52
+ "is_corresponding": a.get("is_corresponding"),
53
+ "institutions": [{"ror": i.get("ror"), "display_name": i.get("display_name"),
54
+ "country_code": i.get("country_code")}
55
+ for i in a.get("institutions") or []],
56
+ "countries": a.get("countries")}
57
+ for a in w.get("authorships") or []],
58
+ }
59
+
60
+
61
+ def works_raw(search=None, *, ror=None, doi=None, title_search=None,
62
+ per_page=25, cursor=None, select=None, cache=None, no_cache=False):
63
+ """→ {total, count, rows, next_cursor, params, _meta}; /works direct, verbatim name.
64
+ search full-text × ror (exact institutions.ror) × doi × title_search (title.search) — the
65
+ four constraints combine with AND into filter; at least one constraint (local wall: a
66
+ cursor is not a constraint, a bare whole-corpus scan is served by the bulk pipeline);
67
+ per_page 1-200 (explicit upstream 400 wall); cursor=meta.next_cursor to continue (it
68
+ encodes paging only, the constraint parameters must be re-sent unchanged); select is
69
+ force-merged with the core fields."""
70
+ from ..transport import call_provider
71
+ from ..errors import SdkError
72
+
73
+ q = str(search or "").strip() or None
74
+ r_v = _norm_ror(ror)
75
+ d_v = str(doi or "").strip() or None
76
+ t_v = str(title_search or "").strip() or None
77
+ if not (q or r_v or d_v or t_v):
78
+ raise SdkError("usage_error", provider="openalex",
79
+ detail="openalex_works requires at least one of search/ror/doi/title_search"
80
+ " (a cursor is not a constraint; local safety wall: a bare whole-corpus "
81
+ "scan is served by the bulk pipeline)")
82
+ pp = int(per_page)
83
+ if not 1 <= pp <= _PER_PAGE_CAP:
84
+ raise SdkError("usage_error", provider="openalex",
85
+ detail=f"per_page must be 1-{_PER_PAGE_CAP} (explicit upstream 400 wall, confirmed by probing 2026-10-26)")
86
+ filters = []
87
+ if r_v:
88
+ filters.append(f"institutions.ror:{r_v}")
89
+ if d_v:
90
+ filters.append(f"doi:{d_v}")
91
+ if t_v:
92
+ filters.append(f"title.search:{t_v}")
93
+ params = {"per-page": pp}
94
+ if q:
95
+ params["search"] = q
96
+ if filters:
97
+ params["filter"] = ",".join(filters)
98
+ cur = str(cursor or "").strip() or None
99
+ if cur:
100
+ params["cursor"] = cur
101
+ sel = {x.strip() for x in (select if isinstance(select, (list, tuple))
102
+ else str(select or "").split(",")) if x and str(x).strip()}
103
+ params["select"] = ",".join(sorted(sel | set(_CORE_FIELDS.split(","))))
104
+ r = call_provider(provider, "works", params, cache=cache, no_cache=no_cache)
105
+ data = r["data"] or {}
106
+ meta = data.get("meta") or {}
107
+ rows = [_work_row(w) for w in data.get("results") or []]
108
+ return {"total": meta.get("count"), "count": len(rows), "rows": rows,
109
+ "next_cursor": meta.get("next_cursor"), "params": params, "_meta": r}
110
+
111
+
112
+ def _institution_row(i):
113
+ return {
114
+ "id": i.get("id"), "ror": i.get("ror"), "display_name": i.get("display_name"),
115
+ "country_code": i.get("country_code"), "type": i.get("type"),
116
+ "works_count": i.get("works_count"), "cited_by_count": i.get("cited_by_count"),
117
+ "relevance_score": i.get("relevance_score"),
118
+ "homepage_url": i.get("homepage_url"),
119
+ }
120
+
121
+
122
+ def institutions_raw(search, *, per_page=25, cache=None, no_cache=False):
123
+ """→ {total, count, rows, params, _meta}; /institutions?search= direct, verbatim name.
124
+ search required (local wall); per_page 1-200 (same wall as works).
125
+ Rows = institution candidate cards (ror/type/works_count); same-name clones in different
126
+ countries coexist (a pfizer probe returned 24 candidates incl. US/UK/FR) — the first one is
127
+ never auto-picked; pick the ROR explicitly and then query output via openalex_works
128
+ (adversarial-review constraint 1)."""
129
+ from ..transport import call_provider
130
+ from ..errors import SdkError
131
+
132
+ q = str(search or "").strip()
133
+ if not q:
134
+ raise SdkError("usage_error", provider="openalex", detail="openalex_institutions requires search (institution name)")
135
+ pp = int(per_page)
136
+ if not 1 <= pp <= _PER_PAGE_CAP:
137
+ raise SdkError("usage_error", provider="openalex",
138
+ detail=f"per_page must be 1-{_PER_PAGE_CAP} (explicit upstream 400 wall)")
139
+ params = {"search": q, "per-page": pp}
140
+ r = call_provider(provider, "institutions", params, cache=cache, no_cache=no_cache)
141
+ data = r["data"] or {}
142
+ meta = data.get("meta") or {}
143
+ rows = [_institution_row(i) for i in data.get("results") or []]
144
+ return {"total": meta.get("count"), "count": len(rows), "rows": rows,
145
+ "params": params, "_meta": r}
@@ -0,0 +1,50 @@
1
+ """opencitations — OpenCitations COCI (open citation index; keyless).
2
+
3
+ No key needed (migrated to the new host api.opencitations.net/index/v1/ — the old
4
+ opencitations.org paths 301 here). citations face (probed 2026-10-26): GET /citations/{doi}
5
+ (the DOI carries **no** doi: prefix; with it you get a 400 whose error body reports the
6
+ correct format); **the limit parameter is silently ignored** (?limit=3 still returns all 217
7
+ rows — one more case of an upstream silent clamp), so this face sets no limit and returns the
8
+ full list plus a count.
9
+ Row keys: oci/citing/cited/creation/timespan/journal_sc/author_sc.
10
+ """
11
+ provider = {
12
+ "id": "opencitations",
13
+ "baseUrl": "https://api.opencitations.net",
14
+ "rate": {"rps": 1, "burst": 5}, # small academic-service tier (same as osv/nsf; COCI is single-shot with no paging, naturally low traffic)
15
+ "ops": {
16
+ "citations": {"path": "/index/v1/citations/{doi}", "method": "GET", "idempotent": True},
17
+ },
18
+ "ttl": {"citations": 24 * 3600},
19
+ }
20
+
21
+
22
+ def _norm_doi(doi):
23
+ """DOI normalization: strip the doi: prefix (upstream 400 wall — the error body itself
24
+ reports the prefix-less format)."""
25
+ v = str(doi or "").strip().lower()
26
+ if v.startswith("doi:"):
27
+ v = v[4:].strip()
28
+ if v.startswith("https://doi.org/"):
29
+ v = v[len("https://doi.org/"):]
30
+ if v.startswith("doi.org/"):
31
+ v = v[len("doi.org/"):]
32
+ return v or None
33
+
34
+
35
+ def citations_raw(doi, *, cache=None, no_cache=False):
36
+ """→ {doi, count, rows, params, _meta}; /index/v1/citations/{doi} direct, verbatim name
37
+ (full list). doi required (doi: prefix/doi.org URL normalized away automatically); the
38
+ upstream limit parameter is silently ignored (probe: ?limit=3 still returned all 217
39
+ rows) — no fake pagination, the full list comes back."""
40
+ from ..transport import call_provider
41
+ from ..errors import SdkError
42
+
43
+ v = _norm_doi(doi)
44
+ if not v:
45
+ raise SdkError("usage_error", provider="opencitations",
46
+ detail="opencitations_citations requires doi (the cited DOI; the doi: prefix is auto-stripped)")
47
+ params = {"doi": v}
48
+ r = call_provider(provider, "citations", params, cache=cache, no_cache=no_cache)
49
+ rows = r["data"] or []
50
+ return {"doi": v, "count": len(rows), "rows": rows, "params": params, "_meta": r}
@@ -0,0 +1,3 @@
1
+ """security domain package — provider adapters laid flat (one source per file) + faces/cli (0.2.6 per-domain split).
2
+ """
3
+ from . import cisa_kev, osv # noqa: F401
@@ -0,0 +1,42 @@
1
+ """cisa_kev — CISA Known Exploited Vulnerabilities (KEV) Catalog provider (catalog of vulnerabilities
2
+ exploited in the wild; keyless).
3
+
4
+ Keyless static feed (~1.8MB single shot, probed 2026-10-26, count=1756). Upstream takes no query params —
5
+ all cve/ransomware filtering is a **local post-filter** (echoed in input; upstream stays pure with empty params).
6
+ The catalog updates at weekly cadence (the catalogVersion date stamp goes into reason).
7
+ """
8
+ provider = {
9
+ "id": "cisa-kev",
10
+ "baseUrl": "https://www.cisa.gov",
11
+ "rate": {"rps": 0.25, "burst": 2},
12
+ "ops": {
13
+ "catalog": {"path": "/sites/default/files/feeds/known_exploited_vulnerabilities.json",
14
+ "method": "GET", "idempotent": True},
15
+ },
16
+ "ttl": {"catalog": 24 * 3600},
17
+ }
18
+
19
+
20
+ def _row(v):
21
+ return {
22
+ "cve_id": v.get("cveID"), "vendor_project": v.get("vendorProject"),
23
+ "product": v.get("product"), "vulnerability_name": v.get("vulnerabilityName"),
24
+ "date_added": v.get("dateAdded"), "due_date": v.get("dueDate"),
25
+ "known_ransomware_campaign_use": v.get("knownRansomwareCampaignUse"),
26
+ "short_description": v.get("shortDescription"),
27
+ "required_action": v.get("requiredAction"), "notes": v.get("notes"),
28
+ "cwes": v.get("cwes") or [],
29
+ }
30
+
31
+
32
+ def catalog_raw(*, cache=None, no_cache=False):
33
+ """→ {catalog_version, date_released, count_total, vulnerabilities, _meta}; the feed reshaped as-is.
34
+ No upstream params (static feed); the local post-filter happens at the face layer (the provider keeps the
35
+ zero-hidden-params discipline)."""
36
+ from ..transport import call_provider
37
+
38
+ r = call_provider(provider, "catalog", {}, cache=cache, no_cache=no_cache)
39
+ d = r["data"] or {}
40
+ vulns = [_row(v) for v in d.get("vulnerabilities") or []]
41
+ return {"catalog_version": d.get("catalogVersion"), "date_released": d.get("dateReleased"),
42
+ "count_total": d.get("count"), "vulnerabilities": vulns, "_meta": r}
@@ -0,0 +1,37 @@
1
+ """security cli — domain command registration (split per domain in 0.2.6, moved in from the root cli.py).
2
+
3
+ register(v, g): registers the flat commands on the domain subparser (g = the root _leaf_globals factory,
4
+ passed in to avoid a cli↔domain-cli cycle); FLAT = the flat dispatch table, merged by the root cli for main.
5
+ """
6
+ import argparse
7
+
8
+ def register(v, g):
9
+ f1 = v.add_parser("cisa_kev_catalog", parents=[g()],
10
+ description="CISA Known Exploited Vulnerabilities catalog (realtime, local execution; static feed ~1.8MB; --cve and --ransomware are local post-filters, upstream stays empty-param pure; catalog updates weekly, catalogVersion echoed in reason)")
11
+ f1.add_argument("--cve", default=None, help="CVE id exact lookup (local filter, e.g. CVE-2021-44228)")
12
+ f1.add_argument("--ransomware", action="store_true",
13
+ help="Only entries with known ransomware campaign use (local filter)")
14
+ f1.add_argument("--limit", type=int, default=50, help="Rows to return (default 50; full catalog ~1,756 rows — page instead of dumping)")
15
+
16
+ f2 = v.add_parser("osv_vulns", parents=[g()],
17
+ description="Open-source package vulnerability query (realtime, local execution; OSV /v1/query POST; ecosystem is a closed set validated locally because upstream silently returns empty for unknown values; version omitted = all known vulns for the package)")
18
+ f2.add_argument("--package", required=True, help="Package name (e.g. lodash)")
19
+ f2.add_argument("--ecosystem", required=True,
20
+ help="OSV ecosystem (npm/PyPI/Go/Maven/crates.io/RubyGems/NuGet/Packagist/Debian/Alpine/Linux/OSS-Fuzz/Hex/Pub/Conda/Android/GitHub Actions)")
21
+ f2.add_argument("--version", default=None, help="Package version (omit = all known vulns)")
22
+ f2.add_argument("--page-token", dest="page_token", default=None,
23
+ help="Continuation token from a prior response's result.next_page_token (upstream paginates at >1000 vulns or >20s)")
24
+
25
+ FLAT = {
26
+ "cisa_kev_catalog": lambda a: {"cve": a.cve, "ransomware": a.ransomware, "limit": a.limit},
27
+ "osv_vulns": lambda a: {"package": a.package, "ecosystem": a.ecosystem,
28
+ "version": a.version, "page_token": a.page_token},
29
+ }
30
+
31
+
32
+ # 0.3.10 jurisdiction chapter: face -> jurisdiction (default = the issuing authority's jurisdiction;
33
+ # institutions covering global phenomena = GLOBAL; vocabulary = ISO-3166 alpha-2 + EU + GLOBAL) — the root cli.py merges this into _JURISDICTION; the success envelope carries a jurisdiction chapter
34
+ COUNTRY = {
35
+ "cisa_kev_catalog": "US",
36
+ "osv_vulns": "GLOBAL",
37
+ }
@@ -0,0 +1,91 @@
1
+ """security faces — flat face-method mixin (split per domain in 0.2.6, moved in from client.py _Domain).
2
+
3
+ Mixed into the root _Domain (assembled via MRO, see client.py); discipline: no __init__, depends only on the
4
+ root internals (_flat_env/_c/_request…), method names globally unique across domains (a collision
5
+ would shadow another domain's face).
6
+ """
7
+ from ..errors import SdkError
8
+
9
+
10
+ from .. import security # domain-module path call (historical shape, kept for minimal disturbance)
11
+
12
+ class SecurityFaces:
13
+ def _security_only(self, tool: str):
14
+ if self.name != "security":
15
+ raise SdkError("usage_error", detail=f"{tool} is a security-domain capability (not available on the {self.name} domain)")
16
+
17
+ def osv_vulns(self, *, package, ecosystem, version=None, page_token=None,
18
+ execution=None, no_cache=None):
19
+ """security osv_vulns: open-source package vulnerability query (OSV /v1/query POST with verbatim
20
+ param names; ecosystem closed-set check — upstream silently returns empty for unknown values, so the
21
+ false-negative guard lives locally; version may be omitted = all known vulns;
22
+ paging: a next_page_token present in the response = more to fetch, pass page_token back verbatim to
23
+ continue — upstream paginates past >1000 vulns or >20s (probed 2026-10-28: Linux/Kernel 2719 rows
24
+ with a token)."""
25
+ self._security_only("osv_vulns")
26
+ r = security.osv.query_raw(package, ecosystem, version=version, page_token=page_token,
27
+ cache=self._c._cache,
28
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
29
+ result = {k: x for k, x in r.items() if k != "_meta"}
30
+ pk = r["params"]["package"]
31
+ ver = r["params"].get("version")
32
+ top = (result["vulns"] or [None])[0]
33
+ return self._flat_env(
34
+ tool="osv_vulns",
35
+ input={"package": pk["name"], "ecosystem": pk["ecosystem"],
36
+ "version": ver, "page_token": r["params"].get("page_token")},
37
+ reason=(f"OSV: {pk['ecosystem']} package \"{pk['name']}\""
38
+ + (f"@{ver} " if ver else "")
39
+ + f" has {result['count']} known vulnerabilities"
40
+ + (f", top concern {top['id']} ({top['summary'] or '—'})" if top else " (zero hits = no known vulnerabilities recorded for this version)")
41
+ + (" (more available — pass result.next_page_token back as --page-token to continue)" if result.get("next_page_token") else "")),
42
+ result=result,
43
+ evidence=[{"quote": f"{v['id']} = {v['summary'] or '—'}"
44
+ + (f" (alias {'/'.join(v['aliases'][:2])})" if v["aliases"] else ""),
45
+ "url": v["advisory_url"] or f"https://osv.dev/vulnerability/{v['id']}"}
46
+ for v in result["vulns"][:5]],
47
+ provider_id="osv", metas=[r["_meta"]],
48
+ query={"source": "raw", "upstream": r["params"]},
49
+ )
50
+
51
+ def cisa_kev_catalog(self, *, cve=None, ransomware=None, limit=50,
52
+ execution=None, no_cache=None):
53
+ """security cisa_kev_catalog: the CISA catalog of vulnerabilities exploited in the wild (static feed
54
+ ~1.8MB; cve exact lookup / ransomware=True lists only known ransomware use — both are local
55
+ post-filters, upstream always stays pure with empty params; the catalog updates weekly, catalogVersion
56
+ goes into reason)."""
57
+ self._security_only("cisa_kev_catalog")
58
+ r = security.cisa_kev.catalog_raw(cache=self._c._cache,
59
+ no_cache=self._c.no_cache if no_cache is None else bool(no_cache))
60
+ all_rows = r["vulnerabilities"]
61
+ rows = all_rows
62
+ if ransomware:
63
+ rows = [v for v in rows if v["known_ransomware_campaign_use"] == "Known"]
64
+ cv = str(cve or "").strip().upper() or None
65
+ if cv:
66
+ rows = [v for v in rows if (v["cve_id"] or "").upper() == cv]
67
+ rows = rows[:max(1, int(limit))]
68
+ result = {"catalog_version": r["catalog_version"], "date_released": r["date_released"],
69
+ "count_total": r["count_total"], "count": len(rows), "vulnerabilities": rows}
70
+ filters = []
71
+ if cv:
72
+ filters.append(f"CVE {cv}")
73
+ if ransomware:
74
+ filters.append("ransomware use on record")
75
+ return self._flat_env(
76
+ tool="cisa_kev_catalog",
77
+ input={"cve": cve, "ransomware": bool(ransomware), "limit": int(limit)},
78
+ reason=(f"CISA KEV catalog v{r['catalog_version']} (released {r['date_released'] or '—'}, "
79
+ f"{r['count_total']} entries in total)"
80
+ + (f", {len(rows)} entries matching {'+'.join(filters)}" if filters else f", returning the first {len(rows)} entries")
81
+ + " — proven exploitation in the wild = the highest CVE priority tier"),
82
+ result=result,
83
+ evidence=[{"quote": f"{v['cve_id']} = {v['vendor_project'] or '—'} {v['product'] or '—'}: "
84
+ f"{v['vulnerability_name'] or '—'}"
85
+ + (" (ransomware use on record)" if v["known_ransomware_campaign_use"] == "Known" else ""),
86
+ "url": f"https://nvd.nist.gov/vuln/detail/{v['cve_id']}"}
87
+ for v in result["vulnerabilities"][:5]],
88
+ provider_id="cisa-kev", metas=[r["_meta"]],
89
+ query=({**({"source": "assembled", "assembled_from": {"cve": cv, "ransomware": bool(ransomware)}}),
90
+ "upstream": {}} if (cv or ransomware) else {"source": "raw", "upstream": {}}),
91
+ )
@@ -0,0 +1,72 @@
1
+ """osv — OSV.dev API provider (open-source package vulnerability queries; keyless).
2
+
3
+ Keyless (the api.osv.dev public query face, polite rate 1 rps). POST /v1/query:
4
+ package{name,ecosystem}+version (version may be omitted = all known vulns for the package — upstream semantics);
5
+ upstream returns an empty vulns[] for an unknown ecosystem (**a silent false negative**) — this face blocks that
6
+ with a closed-set check locally (the OSV docs enumeration).
7
+ The single-vuln detail face is GET /v1/vulns/{id} (keyless); no face stood up for it this round
8
+ (the screening face already covers the use cases).
9
+ """
10
+ provider = {
11
+ "id": "osv",
12
+ "baseUrl": "https://api.osv.dev",
13
+ "rate": {"rps": 1, "burst": 5},
14
+ "ops": {
15
+ "query": {"path": "/v1/query", "method": "POST", "idempotent": True},
16
+ },
17
+ "ttl": {"query": 24 * 3600},
18
+ }
19
+
20
+ # OSV docs ecosystem enumeration (normalized case-insensitively to the canonical form); upstream silently
21
+ # returns empty for unknown values — the false-negative guard lives here
22
+ ECOSYSTEMS = {
23
+ "npm": "npm", "pypi": "PyPI", "go": "Go", "maven": "Maven", "crates.io": "crates.io",
24
+ "rubygems": "RubyGems", "nuget": "NuGet", "packagist": "Packagist", "debian": "Debian",
25
+ "alpine": "Alpine", "linux": "Linux", "oss-fuzz": "OSS-Fuzz", "hex": "Hex",
26
+ "pub": "Pub", "conda": "Conda", "android": "Android", "github actions": "GitHub Actions",
27
+ }
28
+
29
+
30
+ def query_raw(package, ecosystem, *, version=None, page_token=None,
31
+ cache=None, no_cache=False):
32
+ """→ {count, vulns, next_page_token, params, _meta}; /v1/query POST with verbatim names.
33
+ ecosystem closed-set check (case-insensitive → canonical form); version may be omitted = all known vulns
34
+ (upstream semantics).
35
+ Paging: upstream paginates past >1000 vulns or >20s (official docs), the response carries next_page_token —
36
+ surfaced as-is + pass it back as page_token to continue until it goes absent; an occasional
37
+ "token-only, no rows" page must still be continued (per the docs).
38
+ Probed 2026-10-28: Linux/Kernel 2719 rows on the first page + a token present.
39
+ Recorded, not adopted (2026-10-28 audit): purl (an alternative to package{name,ecosystem}, the toolchain
40
+ form) and commit (another key for version lookups) — the screening name+ecosystem form already covers the
41
+ use cases, so no params were added."""
42
+ from ..transport import call_provider
43
+ from ..errors import SdkError
44
+
45
+ name = str(package or "").strip()
46
+ if not name:
47
+ raise SdkError("usage_error", provider="osv", detail="osv_vulns requires package (the package name)")
48
+ eco_canon = ECOSYSTEMS.get(str(ecosystem or "").strip().lower())
49
+ if not eco_canon:
50
+ raise SdkError("usage_error", provider="osv",
51
+ detail=f"ecosystem \"{ecosystem}\" is not in the OSV enumeration ("
52
+ + ", ".join(sorted(set(ECOSYSTEMS.values()))) +
53
+ "; upstream silently returns empty for unknown values — the false-negative guard lives here; treat the OSV docs as authoritative for new values")
54
+ body = {"package": {"name": name, "ecosystem": eco_canon}}
55
+ if version not in (None, ""):
56
+ body["version"] = str(version).strip()
57
+ if page_token not in (None, ""):
58
+ body["page_token"] = str(page_token).strip()
59
+ r = call_provider(provider, "query", body, cache=cache, no_cache=no_cache)
60
+ vulns = (r["data"] or {}).get("vulns") or []
61
+ rows = [{
62
+ "id": v.get("id"),
63
+ "aliases": v.get("aliases") or [],
64
+ "summary": v.get("summary"),
65
+ "published": v.get("published"), "modified": v.get("modified"),
66
+ "severity": v.get("severity") or [],
67
+ "advisory_url": (f"https://github.com/advisories/{v['id']}"
68
+ if str(v.get("id") or "").startswith("GHSA-") else None),
69
+ } for v in vulns]
70
+ return {"count": len(rows), "vulns": rows,
71
+ "next_page_token": (r["data"] or {}).get("next_page_token"),
72
+ "params": body, "_meta": r}