@westbayberry/dg 1.0.13 → 1.0.14

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 (3) hide show
  1. package/README.md +88 -17
  2. package/dist/index.mjs +170 -145
  3. package/package.json +7 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @westbayberry/dg
2
2
 
3
- Supply chain security scanner for npm and Python dependencies. Detects malicious packages, typosquatting, dependency confusion, and 20+ attack patterns before they reach production.
3
+ Supply chain security scanner for npm and Python dependencies. Scans lockfile changes against 26+ detectors to catch malicious packages, typosquatting, dependency confusion, credential theft, and obfuscated code before they reach production.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,6 +8,8 @@ Supply chain security scanner for npm and Python dependencies. Detects malicious
8
8
  npm install -g @westbayberry/dg
9
9
  ```
10
10
 
11
+ Requires Node.js 18+.
12
+
11
13
  ## Quick Start
12
14
 
13
15
  ```bash
@@ -15,48 +17,93 @@ dg login
15
17
  dg scan
16
18
  ```
17
19
 
18
- The CLI auto-discovers projects in your directory tree npm lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) and Python dependency files (requirements.txt, Pipfile.lock, poetry.lock). If multiple projects are found, you pick which ones to scan.
20
+ The CLI walks your directory tree and finds npm lockfiles (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`) and Python dependency files (`requirements.txt`, `Pipfile.lock`, `poetry.lock`). If multiple projects are found, an interactive selector lets you pick which ones to scan.
21
+
22
+ Only changed packages are scanned by default — `dg` diffs your lockfile against the git merge-base with `main` to find what's new or updated.
19
23
 
20
24
  ## Commands
21
25
 
22
26
  ```
23
- dg scan [options] Scan dependencies (auto-discovers npm + Python projects)
24
- dg npm install <pkg> Scan packages before installing
27
+ dg scan [options] Scan dependencies for supply chain threats
28
+ dg npm install <pkg> Scan packages before installing them
25
29
  dg login Authenticate with your WestBayBerry account
26
30
  dg logout Remove saved credentials
27
- dg hook install Install git pre-commit hook to scan lockfile changes
31
+ dg hook install Install git pre-commit hook
28
32
  dg hook uninstall Remove the pre-commit hook
29
33
  dg update Check for and install the latest version
30
34
  dg wrap Show instructions to alias npm to dg
31
35
  ```
32
36
 
33
- ### Scan Options
37
+ ## What It Detects
38
+
39
+ Each package is analyzed by 26+ detectors covering:
40
+
41
+ | Category | Examples |
42
+ |----------|----------|
43
+ | **Code execution** | `child_process` spawning, `eval`/`Function` calls, shell command injection |
44
+ | **Network exfiltration** | HTTP/WebSocket/DNS/gRPC calls, URL obfuscation, data exfil patterns |
45
+ | **Credential theft** | Reading SSH keys, browser tokens, cloud credentials, `.npmrc`/`.pypirc` |
46
+ | **Install scripts** | Suspicious `preinstall`/`postinstall` hooks, download-and-execute chains |
47
+ | **Obfuscation** | Hex/unicode encoding, string reconstruction, phantom eval, minified payloads |
48
+ | **Supply chain** | Typosquatting, dependency confusion, version squatting, borrowed repo URLs |
49
+ | **Persistence** | Writing to shell configs, cron jobs, systemd units, SSH `authorized_keys` |
50
+ | **Behavioral** | Time-gated payloads, purpose mismatch, runtime evasion, binary addons |
51
+ | **Reputation** | Missing/fake GitHub repos, ghost packages, low download counts |
52
+
53
+ Findings include severity (1–5), confidence (0–1), and code evidence with file paths and line numbers.
54
+
55
+ ## Scan Options
34
56
 
35
57
  | Flag | Default | Description |
36
58
  |------|---------|-------------|
37
59
  | `--mode <mode>` | `warn` | `block` / `warn` / `off` |
38
- | `--block-threshold <n>` | `70` | Score threshold for blocking |
39
- | `--warn-threshold <n>` | `60` | Score threshold for warnings |
60
+ | `--block-threshold <n>` | `70` | Score threshold for blocking (0–100) |
61
+ | `--warn-threshold <n>` | `60` | Score threshold for warnings (0–100) |
40
62
  | `--max-packages <n>` | `200` | Max packages per scan |
41
63
  | `--allowlist <pkgs>` | | Comma-separated packages to skip |
42
- | `--json` | | Output JSON for CI parsing |
64
+ | `--json` | | Output raw JSON (for CI parsing) |
43
65
  | `--scan-all` | | Scan all packages, not just changed |
44
66
  | `--base-lockfile <path>` | | Explicit base lockfile for diff |
45
67
  | `--workspace <dir>` | | Scan a specific workspace subdirectory |
46
- | `--debug` | | Show diagnostic output |
68
+ | `--debug` | | Show discovery, batching, and timing info |
47
69
 
48
- ### Exit Codes
70
+ ## Exit Codes
49
71
 
50
72
  | Code | Meaning | CI Action |
51
73
  |------|---------|-----------|
52
74
  | `0` | Pass | Continue |
53
75
  | `1` | Warning | Advisory — review recommended |
54
76
  | `2` | Block | Fail the pipeline |
55
- | `3` | Error | Internal error |
77
+ | `3` | Error | Internal error (auth, network, etc.) |
56
78
 
57
- ## CI Setup
79
+ ## Configuration
80
+
81
+ Settings can come from CLI flags, environment variables, or a `.dgrc.json` config file (searched in the current directory, then `~/`). CLI flags take highest precedence.
82
+
83
+ ### `.dgrc.json`
84
+
85
+ ```json
86
+ {
87
+ "apiKey": "dg_...",
88
+ "mode": "block",
89
+ "blockThreshold": 70,
90
+ "warnThreshold": 60,
91
+ "maxPackages": 200,
92
+ "allowlist": ["known-safe-pkg"]
93
+ }
94
+ ```
95
+
96
+ ### Environment Variables
97
+
98
+ | Variable | Description |
99
+ |----------|-------------|
100
+ | `DG_API_URL` | API base URL |
101
+ | `DG_MODE` | `block` / `warn` / `off` |
102
+ | `DG_ALLOWLIST` | Comma-separated allowlist |
103
+ | `DG_DEBUG` | Set to `1` for diagnostic output |
104
+ | `DG_WORKSPACE` | Workspace subdirectory |
58
105
 
59
- Authenticate first, then add the scan to your pipeline:
106
+ ## CI Setup
60
107
 
61
108
  ### GitHub Actions
62
109
 
@@ -74,6 +121,18 @@ npx @westbayberry/dg login
74
121
  npx @westbayberry/dg scan --mode block --json
75
122
  ```
76
123
 
124
+ The `--json` flag outputs machine-readable results. Exit code `2` signals a blocked scan — wire it into your pipeline to fail the build.
125
+
126
+ ### Monorepo / Workspace
127
+
128
+ Scan a specific workspace:
129
+
130
+ ```bash
131
+ dg scan --workspace packages/api
132
+ ```
133
+
134
+ Or let `dg` discover all projects and pick interactively.
135
+
77
136
  ## Git Hook
78
137
 
79
138
  Block commits that introduce risky dependencies:
@@ -82,22 +141,34 @@ Block commits that introduce risky dependencies:
82
141
  dg hook install
83
142
  ```
84
143
 
85
- This installs a pre-commit hook that runs `dg scan --mode block` whenever a lockfile change is staged. Use `dg hook uninstall` to remove it.
144
+ This adds a pre-commit hook that runs `dg scan --mode block` whenever a lockfile is staged. If any package scores above the block threshold, the commit is rejected. Remove it with `dg hook uninstall`.
86
145
 
87
146
  ## npm Wrapper
88
147
 
89
- Scan packages before installing:
148
+ Scan packages before they're installed:
90
149
 
91
150
  ```bash
92
151
  dg npm install express lodash
93
152
  ```
94
153
 
95
- Use `--dg-force` to bypass a block. Or alias npm globally:
154
+ Packages are resolved and scanned through the API. If a package is blocked, you'll get a confirmation prompt — press `y` to install anyway, or use `--dg-force` to skip the prompt.
155
+
156
+ To make this the default for all `npm install` commands:
96
157
 
97
158
  ```bash
98
159
  echo 'alias npm="dg npm"' >> ~/.zshrc
99
160
  ```
100
161
 
162
+ ## Python Support
163
+
164
+ Python projects are detected alongside npm. The scanner reads:
165
+
166
+ - `requirements.txt` — `name==version` pins
167
+ - `Pipfile.lock` — default and develop sections
168
+ - `poetry.lock` — `[[package]]` entries
169
+
170
+ Python packages are analyzed through the same detection engine against the PyPI registry.
171
+
101
172
  ## Links
102
173
 
103
174
  - [Dashboard](https://westbayberry.com/dashboard)
package/dist/index.mjs CHANGED
@@ -39587,6 +39587,144 @@ var init_lockfile = __esm({
39587
39587
  }
39588
39588
  });
39589
39589
 
39590
+ // src/discover.ts
39591
+ var discover_exports = {};
39592
+ __export(discover_exports, {
39593
+ discoverProjects: () => discoverProjects
39594
+ });
39595
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync, statSync } from "node:fs";
39596
+ import { join as join6, relative, basename } from "node:path";
39597
+ function discoverProjects(root) {
39598
+ const projects = [];
39599
+ walk(root, root, 0, projects);
39600
+ return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
39601
+ }
39602
+ function walk(dir, root, depth, out) {
39603
+ if (depth > MAX_DEPTH) return;
39604
+ for (const lockfile of NPM_LOCKFILES) {
39605
+ const lockPath = join6(dir, lockfile);
39606
+ if (existsSync6(lockPath)) {
39607
+ const count = countNpmPackages(lockPath);
39608
+ if (count > 0) {
39609
+ out.push({
39610
+ path: dir,
39611
+ relativePath: relative(root, dir) || ".",
39612
+ ecosystem: "npm",
39613
+ depFile: lockfile,
39614
+ packageCount: count
39615
+ });
39616
+ }
39617
+ break;
39618
+ }
39619
+ }
39620
+ for (const depFile of PYTHON_DEPFILES) {
39621
+ const depPath = join6(dir, depFile);
39622
+ if (existsSync6(depPath)) {
39623
+ const count = countPythonPackages(depPath, depFile);
39624
+ if (count > 0) {
39625
+ out.push({
39626
+ path: dir,
39627
+ relativePath: relative(root, dir) || ".",
39628
+ ecosystem: "pypi",
39629
+ depFile,
39630
+ packageCount: count
39631
+ });
39632
+ }
39633
+ break;
39634
+ }
39635
+ }
39636
+ let entries;
39637
+ try {
39638
+ entries = readdirSync(dir);
39639
+ } catch {
39640
+ return;
39641
+ }
39642
+ for (const entry of entries) {
39643
+ if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
39644
+ const full = join6(dir, entry);
39645
+ try {
39646
+ if (statSync(full).isDirectory()) {
39647
+ walk(full, root, depth + 1, out);
39648
+ }
39649
+ } catch {
39650
+ }
39651
+ }
39652
+ }
39653
+ function countNpmPackages(lockPath) {
39654
+ try {
39655
+ const name = basename(lockPath);
39656
+ const content = readFileSync7(lockPath, "utf-8");
39657
+ if (name === "yarn.lock") {
39658
+ return (content.match(/^\S.*:$/gm) || []).length;
39659
+ }
39660
+ if (name === "pnpm-lock.yaml") {
39661
+ return (content.match(/^\s{2}'?[/@]/gm) || []).length || estimateLines(content, 200);
39662
+ }
39663
+ const parsed = JSON.parse(content);
39664
+ if (parsed.packages) {
39665
+ return Object.keys(parsed.packages).filter((k) => k !== "").length;
39666
+ }
39667
+ if (parsed.dependencies) {
39668
+ return Object.keys(parsed.dependencies).length;
39669
+ }
39670
+ return 0;
39671
+ } catch {
39672
+ return 0;
39673
+ }
39674
+ }
39675
+ function countPythonPackages(depPath, depFile) {
39676
+ try {
39677
+ const content = readFileSync7(depPath, "utf-8");
39678
+ if (depFile === "Pipfile.lock") {
39679
+ const parsed = JSON.parse(content);
39680
+ const defaultCount = Object.keys(parsed.default || {}).length;
39681
+ const devCount = Object.keys(parsed.develop || {}).length;
39682
+ return defaultCount + devCount;
39683
+ }
39684
+ if (depFile === "poetry.lock") {
39685
+ return (content.match(/^\[\[package\]\]/gm) || []).length;
39686
+ }
39687
+ return content.split("\n").filter(
39688
+ (line) => line.trim() && !line.trim().startsWith("#") && !line.trim().startsWith("-")
39689
+ ).length;
39690
+ } catch {
39691
+ return 0;
39692
+ }
39693
+ }
39694
+ function estimateLines(content, fallback) {
39695
+ const lines = content.split("\n").length;
39696
+ return lines > 20 ? Math.floor(lines / 4) : fallback;
39697
+ }
39698
+ var SKIP_DIRS, NPM_LOCKFILES, PYTHON_DEPFILES, MAX_DEPTH;
39699
+ var init_discover = __esm({
39700
+ "src/discover.ts"() {
39701
+ "use strict";
39702
+ SKIP_DIRS = /* @__PURE__ */ new Set([
39703
+ "node_modules",
39704
+ ".venv",
39705
+ "venv",
39706
+ "__pycache__",
39707
+ ".git",
39708
+ ".tox",
39709
+ ".eggs",
39710
+ "dist",
39711
+ "build",
39712
+ ".next",
39713
+ ".nuxt",
39714
+ "coverage",
39715
+ ".cache",
39716
+ ".pytest_cache",
39717
+ ".mypy_cache",
39718
+ "validation-results",
39719
+ "test-fixtures",
39720
+ "fixtures"
39721
+ ]);
39722
+ NPM_LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json"];
39723
+ PYTHON_DEPFILES = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
39724
+ MAX_DEPTH = 8;
39725
+ }
39726
+ });
39727
+
39590
39728
  // src/static-output.ts
39591
39729
  var static_output_exports = {};
39592
39730
  __export(static_output_exports, {
@@ -39750,7 +39888,24 @@ async function runStatic(config) {
39750
39888
  const discovery = discoverChanges(process.cwd(), config);
39751
39889
  dbg(`discovery method: ${discovery.method}`);
39752
39890
  if (discovery.packages.length === 0) {
39753
- process.stderr.write(import_chalk4.default.dim(" No package changes detected.\n"));
39891
+ const { discoverProjects: discoverProjects2 } = await Promise.resolve().then(() => (init_discover(), discover_exports));
39892
+ const subProjects = discoverProjects2(process.cwd());
39893
+ if (subProjects.length > 0) {
39894
+ process.stderr.write(import_chalk4.default.yellow(" No packages found in current directory, but found projects in subdirectories:\n\n"));
39895
+ for (const proj of subProjects) {
39896
+ const pkgLabel = proj.packageCount === 1 ? "package" : "packages";
39897
+ process.stderr.write(
39898
+ import_chalk4.default.white(` ${proj.ecosystem} `) + import_chalk4.default.cyan(proj.relativePath) + import_chalk4.default.dim(` (${proj.packageCount} ${pkgLabel})
39899
+ `)
39900
+ );
39901
+ }
39902
+ process.stderr.write(import_chalk4.default.dim("\n Use --workspace to scan a specific project:\n"));
39903
+ process.stderr.write(import_chalk4.default.dim(` dg scan --workspace ${subProjects[0].relativePath}${config.scanAll ? " --scan-all" : ""}
39904
+
39905
+ `));
39906
+ } else {
39907
+ process.stderr.write(import_chalk4.default.dim(" No package changes detected.\n"));
39908
+ }
39754
39909
  process.exit(0);
39755
39910
  }
39756
39911
  const packages = discovery.packages.filter(
@@ -40037,14 +40192,14 @@ __export(hook_exports, {
40037
40192
  });
40038
40193
  import { execFileSync as execFileSync2 } from "node:child_process";
40039
40194
  import {
40040
- existsSync as existsSync6,
40041
- readFileSync as readFileSync7,
40195
+ existsSync as existsSync7,
40196
+ readFileSync as readFileSync8,
40042
40197
  writeFileSync as writeFileSync3,
40043
40198
  mkdirSync,
40044
40199
  chmodSync as chmodSync2,
40045
40200
  unlinkSync as unlinkSync2
40046
40201
  } from "node:fs";
40047
- import { join as join6 } from "node:path";
40202
+ import { join as join7 } from "node:path";
40048
40203
  function findGitDir() {
40049
40204
  try {
40050
40205
  return execFileSync2("git", ["rev-parse", "--git-dir"], {
@@ -40057,11 +40212,11 @@ function findGitDir() {
40057
40212
  }
40058
40213
  function installHook() {
40059
40214
  const gitDir = findGitDir();
40060
- const hooksDir = join6(gitDir, "hooks");
40061
- const hookPath = join6(hooksDir, "pre-commit");
40215
+ const hooksDir = join7(gitDir, "hooks");
40216
+ const hookPath = join7(hooksDir, "pre-commit");
40062
40217
  mkdirSync(hooksDir, { recursive: true });
40063
- if (existsSync6(hookPath)) {
40064
- const existing = readFileSync7(hookPath, "utf-8");
40218
+ if (existsSync7(hookPath)) {
40219
+ const existing = readFileSync8(hookPath, "utf-8");
40065
40220
  if (existing.includes(HOOK_MARKER)) {
40066
40221
  process.stderr.write(" Hook already installed.\n");
40067
40222
  return;
@@ -40081,12 +40236,12 @@ function installHook() {
40081
40236
  }
40082
40237
  function uninstallHook() {
40083
40238
  const gitDir = findGitDir();
40084
- const hookPath = join6(gitDir, "hooks", "pre-commit");
40085
- if (!existsSync6(hookPath)) {
40239
+ const hookPath = join7(gitDir, "hooks", "pre-commit");
40240
+ if (!existsSync7(hookPath)) {
40086
40241
  process.stderr.write(" No hook to remove.\n");
40087
40242
  return;
40088
40243
  }
40089
- const content = readFileSync7(hookPath, "utf-8");
40244
+ const content = readFileSync8(hookPath, "utf-8");
40090
40245
  if (!content.includes(HOOK_MARKER)) {
40091
40246
  process.stderr.write(
40092
40247
  " No Dependency Guardian hook found in pre-commit.\n"
@@ -40807,140 +40962,6 @@ var init_NpmWrapperApp = __esm({
40807
40962
  }
40808
40963
  });
40809
40964
 
40810
- // src/discover.ts
40811
- import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync, statSync } from "node:fs";
40812
- import { join as join7, relative, basename } from "node:path";
40813
- function discoverProjects(root) {
40814
- const projects = [];
40815
- walk(root, root, 0, projects);
40816
- return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
40817
- }
40818
- function walk(dir, root, depth, out) {
40819
- if (depth > MAX_DEPTH) return;
40820
- for (const lockfile of NPM_LOCKFILES) {
40821
- const lockPath = join7(dir, lockfile);
40822
- if (existsSync7(lockPath)) {
40823
- const count = countNpmPackages(lockPath);
40824
- if (count > 0) {
40825
- out.push({
40826
- path: dir,
40827
- relativePath: relative(root, dir) || ".",
40828
- ecosystem: "npm",
40829
- depFile: lockfile,
40830
- packageCount: count
40831
- });
40832
- }
40833
- break;
40834
- }
40835
- }
40836
- for (const depFile of PYTHON_DEPFILES) {
40837
- const depPath = join7(dir, depFile);
40838
- if (existsSync7(depPath)) {
40839
- const count = countPythonPackages(depPath, depFile);
40840
- if (count > 0) {
40841
- out.push({
40842
- path: dir,
40843
- relativePath: relative(root, dir) || ".",
40844
- ecosystem: "pypi",
40845
- depFile,
40846
- packageCount: count
40847
- });
40848
- }
40849
- break;
40850
- }
40851
- }
40852
- let entries;
40853
- try {
40854
- entries = readdirSync(dir);
40855
- } catch {
40856
- return;
40857
- }
40858
- for (const entry of entries) {
40859
- if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
40860
- const full = join7(dir, entry);
40861
- try {
40862
- if (statSync(full).isDirectory()) {
40863
- walk(full, root, depth + 1, out);
40864
- }
40865
- } catch {
40866
- }
40867
- }
40868
- }
40869
- function countNpmPackages(lockPath) {
40870
- try {
40871
- const name = basename(lockPath);
40872
- const content = readFileSync8(lockPath, "utf-8");
40873
- if (name === "yarn.lock") {
40874
- return (content.match(/^\S.*:$/gm) || []).length;
40875
- }
40876
- if (name === "pnpm-lock.yaml") {
40877
- return (content.match(/^\s{2}'?[/@]/gm) || []).length || estimateLines(content, 200);
40878
- }
40879
- const parsed = JSON.parse(content);
40880
- if (parsed.packages) {
40881
- return Object.keys(parsed.packages).filter((k) => k !== "").length;
40882
- }
40883
- if (parsed.dependencies) {
40884
- return Object.keys(parsed.dependencies).length;
40885
- }
40886
- return 0;
40887
- } catch {
40888
- return 0;
40889
- }
40890
- }
40891
- function countPythonPackages(depPath, depFile) {
40892
- try {
40893
- const content = readFileSync8(depPath, "utf-8");
40894
- if (depFile === "Pipfile.lock") {
40895
- const parsed = JSON.parse(content);
40896
- const defaultCount = Object.keys(parsed.default || {}).length;
40897
- const devCount = Object.keys(parsed.develop || {}).length;
40898
- return defaultCount + devCount;
40899
- }
40900
- if (depFile === "poetry.lock") {
40901
- return (content.match(/^\[\[package\]\]/gm) || []).length;
40902
- }
40903
- return content.split("\n").filter(
40904
- (line) => line.trim() && !line.trim().startsWith("#") && !line.trim().startsWith("-")
40905
- ).length;
40906
- } catch {
40907
- return 0;
40908
- }
40909
- }
40910
- function estimateLines(content, fallback) {
40911
- const lines = content.split("\n").length;
40912
- return lines > 20 ? Math.floor(lines / 4) : fallback;
40913
- }
40914
- var SKIP_DIRS, NPM_LOCKFILES, PYTHON_DEPFILES, MAX_DEPTH;
40915
- var init_discover = __esm({
40916
- "src/discover.ts"() {
40917
- "use strict";
40918
- SKIP_DIRS = /* @__PURE__ */ new Set([
40919
- "node_modules",
40920
- ".venv",
40921
- "venv",
40922
- "__pycache__",
40923
- ".git",
40924
- ".tox",
40925
- ".eggs",
40926
- "dist",
40927
- "build",
40928
- ".next",
40929
- ".nuxt",
40930
- "coverage",
40931
- ".cache",
40932
- ".pytest_cache",
40933
- ".mypy_cache",
40934
- "validation-results",
40935
- "test-fixtures",
40936
- "fixtures"
40937
- ]);
40938
- NPM_LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json"];
40939
- PYTHON_DEPFILES = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
40940
- MAX_DEPTH = 8;
40941
- }
40942
- });
40943
-
40944
40965
  // src/ui/hooks/useScan.ts
40945
40966
  function reducer3(_state, action) {
40946
40967
  switch (action.type) {
@@ -40973,6 +40994,10 @@ function useScan(config) {
40973
40994
  const discovery = discoverChanges(process.cwd(), config);
40974
40995
  const packages = discovery.packages.filter((p) => !config.allowlist.includes(p.name));
40975
40996
  if (packages.length === 0) {
40997
+ if (discovery.packages.length === 0 && projects.length > 0) {
40998
+ dispatch({ type: "PROJECTS_FOUND", projects });
40999
+ return;
41000
+ }
40976
41001
  const message = discovery.packages.length === 0 ? "No package changes detected." : "All changed packages are allowlisted.";
40977
41002
  dispatch({ type: "DISCOVERY_EMPTY", message });
40978
41003
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "1.0.13",
4
- "description": "Supply chain security scanner scan npm dependencies in any CI or terminal",
3
+ "version": "1.0.14",
4
+ "description": "Supply chain security scanner for npm and Python dependencies detects malicious packages, typosquatting, dependency confusion, and 26+ attack patterns",
5
5
  "bin": {
6
6
  "dependency-guardian": "dist/index.mjs",
7
7
  "dg": "dist/index.mjs"
@@ -15,10 +15,14 @@
15
15
  "keywords": [
16
16
  "security",
17
17
  "npm",
18
+ "pypi",
18
19
  "supply-chain",
19
20
  "scanner",
20
21
  "cli",
21
- "dependencies"
22
+ "dependencies",
23
+ "malware",
24
+ "typosquatting",
25
+ "dependency-confusion"
22
26
  ],
23
27
  "publishConfig": {
24
28
  "access": "public"