pmsec 0.2.2__tar.gz → 0.4.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.
- {pmsec-0.2.2 → pmsec-0.4.0}/.gitignore +1 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/PKG-INFO +9 -8
- {pmsec-0.2.2 → pmsec-0.4.0}/README.md +6 -5
- {pmsec-0.2.2 → pmsec-0.4.0}/pyproject.toml +3 -3
- pmsec-0.4.0/src/pmsec/__init__.py +8 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/cli.py +24 -8
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/bun.py +2 -1
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/cargo.py +2 -1
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/mise.py +9 -2
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/npm.py +9 -2
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/pnpm.py +10 -2
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/uv.py +2 -1
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/yarn.py +9 -2
- pmsec-0.4.0/src/pmsec/util/extras.py +33 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/tests/test_cli.py +49 -4
- {pmsec-0.2.2 → pmsec-0.4.0}/uv.lock +1 -1
- pmsec-0.2.2/src/pmsec/__init__.py +0 -1
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/__main__.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/tools/__init__.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/util/__init__.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/util/io.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/util/lines.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/util/paths.py +0 -0
- {pmsec-0.2.2 → pmsec-0.4.0}/src/pmsec/util/version.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmsec
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Inspect and apply install-time cooldown
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Inspect and apply install-time cooldown for npm, pnpm, yarn, bun, cargo, mise, and uv.
|
|
5
5
|
Project-URL: Homepage, https://github.com/HikaruEgashira/pmsec
|
|
6
6
|
Project-URL: Repository, https://github.com/HikaruEgashira/pmsec
|
|
7
7
|
Project-URL: Issues, https://github.com/HikaruEgashira/pmsec/issues
|
|
8
8
|
Author-email: Hikaru Egashira <ai@egahika.dev>
|
|
9
9
|
License: MIT
|
|
10
|
-
Keywords: cooldown,exclude-newer,min-release-age,npm,pmsec,supply-chain,uv
|
|
10
|
+
Keywords: bun,cargo,cooldown,exclude-newer,min-release-age,mise,npm,pmsec,pnpm,supply-chain,uv,yarn
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
|
|
@@ -27,9 +27,9 @@ uvx pmsec unset
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npx
|
|
31
|
-
npx
|
|
32
|
-
npx
|
|
30
|
+
npx pmsec check --min 7
|
|
31
|
+
npx pmsec set 7
|
|
32
|
+
npx pmsec unset
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
If your environment already enforces cooldown (or routes through a proxy
|
|
@@ -37,7 +37,7 @@ index), bootstrap pmsec by overriding just for that call:
|
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
|
|
40
|
-
npx --registry=https://registry.npmjs.org/ --min-release-age=0
|
|
40
|
+
npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Supported tools
|
|
@@ -49,8 +49,9 @@ npm, pnpm, yarn 4+, bun, cargo (RFC #3801), mise, uv
|
|
|
49
49
|
| Command | Description |
|
|
50
50
|
| --- | --- |
|
|
51
51
|
| `pmsec check [--min N]` | Read each tool's config; exit 1 if any tool is below `N` days or unset |
|
|
52
|
-
| `pmsec set <DAYS
|
|
52
|
+
| `pmsec set <DAYS>` | Write `DAYS`-day cooldown to every selected tool |
|
|
53
53
|
| `pmsec unset` | Remove only the cooldown key from each config (other keys preserved) |
|
|
54
|
+
| `pmsec --version` | Print the installed pmsec version |
|
|
54
55
|
|
|
55
56
|
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--json`.
|
|
56
57
|
|
|
@@ -14,9 +14,9 @@ uvx pmsec unset
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx
|
|
18
|
-
npx
|
|
19
|
-
npx
|
|
17
|
+
npx pmsec check --min 7
|
|
18
|
+
npx pmsec set 7
|
|
19
|
+
npx pmsec unset
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
If your environment already enforces cooldown (or routes through a proxy
|
|
@@ -24,7 +24,7 @@ index), bootstrap pmsec by overriding just for that call:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
uvx --index https://pypi.org/simple --exclude-newer-package pmsec=2099-01-01 pmsec check
|
|
27
|
-
npx --registry=https://registry.npmjs.org/ --min-release-age=0
|
|
27
|
+
npx --registry=https://registry.npmjs.org/ --min-release-age=0 pmsec check
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
## Supported tools
|
|
@@ -36,8 +36,9 @@ npm, pnpm, yarn 4+, bun, cargo (RFC #3801), mise, uv
|
|
|
36
36
|
| Command | Description |
|
|
37
37
|
| --- | --- |
|
|
38
38
|
| `pmsec check [--min N]` | Read each tool's config; exit 1 if any tool is below `N` days or unset |
|
|
39
|
-
| `pmsec set <DAYS
|
|
39
|
+
| `pmsec set <DAYS>` | Write `DAYS`-day cooldown to every selected tool |
|
|
40
40
|
| `pmsec unset` | Remove only the cooldown key from each config (other keys preserved) |
|
|
41
|
+
| `pmsec --version` | Print the installed pmsec version |
|
|
41
42
|
|
|
42
43
|
Options: `--tool npm,pnpm,yarn,bun,cargo,mise,uv`, `--json`.
|
|
43
44
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pmsec"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Inspect and apply install-time cooldown
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Inspect and apply install-time cooldown for npm, pnpm, yarn, bun, cargo, mise, and uv."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
8
|
authors = [{ name = "Hikaru Egashira", email = "ai@egahika.dev" }]
|
|
9
|
-
keywords = ["supply-chain", "cooldown", "min-release-age", "exclude-newer", "npm", "uv", "pmsec"]
|
|
9
|
+
keywords = ["supply-chain", "cooldown", "min-release-age", "exclude-newer", "npm", "pnpm", "yarn", "bun", "cargo", "mise", "uv", "pmsec"]
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
12
12
|
Homepage = "https://github.com/HikaruEgashira/pmsec"
|
|
@@ -6,6 +6,7 @@ import shlex
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
from pmsec import __version__
|
|
9
10
|
from pmsec.tools import bun, cargo, mise, npm, pnpm, uv, yarn
|
|
10
11
|
from pmsec.util.paths import current_platform
|
|
11
12
|
|
|
@@ -27,6 +28,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
27
28
|
epilog=USAGE_EPILOG,
|
|
28
29
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
29
30
|
)
|
|
31
|
+
p.add_argument("-V", "--version", action="version", version=f"pmsec {__version__}")
|
|
30
32
|
sub = p.add_subparsers(dest="command", required=True)
|
|
31
33
|
|
|
32
34
|
common = argparse.ArgumentParser(add_help=False)
|
|
@@ -67,7 +69,9 @@ def _gather(targets, env, home, platform):
|
|
|
67
69
|
rows = []
|
|
68
70
|
for t in targets:
|
|
69
71
|
r = t.read(env, home, platform)
|
|
70
|
-
|
|
72
|
+
row = {"tool": t.NAME, "key": t.KEY, "warn": _preflight_warn(t), **r}
|
|
73
|
+
row.setdefault("extras", [])
|
|
74
|
+
rows.append(row)
|
|
71
75
|
return rows
|
|
72
76
|
|
|
73
77
|
|
|
@@ -83,20 +87,31 @@ def _render_human(rows, min_days):
|
|
|
83
87
|
out.append(f"{status} {r['tool']:<4} {r['key']} = {r['configured'] or '(unset)'} [{r['path']}]")
|
|
84
88
|
if r.get("warn"):
|
|
85
89
|
out.append(f" ⚠ {r['warn']}")
|
|
90
|
+
for e in r.get("extras", []):
|
|
91
|
+
if e["configured"] is None:
|
|
92
|
+
ex_status = "MISSING"
|
|
93
|
+
elif e["ok"]:
|
|
94
|
+
ex_status = "OK "
|
|
95
|
+
else:
|
|
96
|
+
ex_status = "STALE "
|
|
97
|
+
out.append(f"{ex_status} {r['tool']:<4} {e['key']} = {e['configured'] or '(unset)'} [{r['path']}]")
|
|
86
98
|
return "\n".join(out) + "\n"
|
|
87
99
|
|
|
88
100
|
|
|
89
101
|
def _check(args, targets, env, home, platform, out, err):
|
|
90
102
|
rows = _gather(targets, env, home, platform)
|
|
91
|
-
|
|
103
|
+
failing_primary = [r for r in rows if r["days"] is None or r["days"] < args.min]
|
|
104
|
+
failing_extras = [e for r in rows for e in r.get("extras", []) if not e["ok"]]
|
|
105
|
+
ok = not failing_primary and not failing_extras
|
|
92
106
|
if args.json:
|
|
93
|
-
out.write(json.dumps({"min": args.min, "rows": rows, "ok":
|
|
107
|
+
out.write(json.dumps({"min": args.min, "rows": rows, "ok": ok}, indent=2) + "\n")
|
|
94
108
|
else:
|
|
95
109
|
out.write(_render_human(rows, args.min))
|
|
96
|
-
if
|
|
97
|
-
err.write(f"pmsec: {len(
|
|
98
|
-
|
|
99
|
-
|
|
110
|
+
if failing_primary:
|
|
111
|
+
err.write(f"pmsec: {len(failing_primary)} tool(s) below {args.min} days\n")
|
|
112
|
+
if failing_extras:
|
|
113
|
+
err.write(f"pmsec: {len(failing_extras)} hardening setting(s) not at safe value\n")
|
|
114
|
+
return 0 if ok else 1
|
|
100
115
|
|
|
101
116
|
|
|
102
117
|
def _explain_fs_error(exc: BaseException, tool: str) -> str:
|
|
@@ -116,7 +131,8 @@ def _explain_fs_error(exc: BaseException, tool: str) -> str:
|
|
|
116
131
|
|
|
117
132
|
def _set(args, targets, env, home, platform, out, err):
|
|
118
133
|
if args.days <= 0:
|
|
119
|
-
|
|
134
|
+
err.write("pmsec: set requires integer DAYS > 0\n")
|
|
135
|
+
return 2
|
|
120
136
|
results = []
|
|
121
137
|
failures = []
|
|
122
138
|
warnings = []
|
|
@@ -12,6 +12,7 @@ KEY = "minimumReleaseAge"
|
|
|
12
12
|
SECTION = "install"
|
|
13
13
|
DOCS = "https://bun.com/docs/runtime/bunfig#install"
|
|
14
14
|
MIN_BIN = (1, 3, 0)
|
|
15
|
+
EXTRAS: list[dict] = []
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def path(env: dict[str, str], home: Path, platform: str) -> Path:
|
|
@@ -42,7 +43,7 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
42
43
|
except ValueError:
|
|
43
44
|
seconds = None
|
|
44
45
|
days = None if seconds is None else seconds // 86400
|
|
45
|
-
return {"path": str(p), "configured": value, "days": days}
|
|
46
|
+
return {"path": str(p), "configured": value, "days": days, "extras": []}
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
@@ -11,6 +11,7 @@ NAME = "cargo"
|
|
|
11
11
|
KEY = "minimum-release-age"
|
|
12
12
|
SECTION = "install"
|
|
13
13
|
DOCS = "https://rust-lang.github.io/rfcs/3801-package-cooldown.html"
|
|
14
|
+
EXTRAS: list[dict] = []
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def path(env: dict[str, str], home: Path, platform: str) -> Path:
|
|
@@ -31,7 +32,7 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
31
32
|
p = path(env, home, platform)
|
|
32
33
|
raw = p.read_text("utf-8") if p.exists() else ""
|
|
33
34
|
value = read_key(raw, KEY, section=SECTION)
|
|
34
|
-
return {"path": str(p), "configured": value, "days": _parse_days(value)}
|
|
35
|
+
return {"path": str(p), "configured": value, "days": _parse_days(value), "extras": []}
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from pmsec.util.extras import apply_extras, read_extras, remove_extras
|
|
6
7
|
from pmsec.util.io import write_atomic
|
|
7
8
|
from pmsec.util.lines import read_key, remove_key, set_key
|
|
8
9
|
from pmsec.util.paths import mise_config_path
|
|
@@ -13,6 +14,9 @@ KEY = "minimum_release_age"
|
|
|
13
14
|
SECTION = "settings"
|
|
14
15
|
DOCS = "https://mise.jdx.dev/configuration/settings.html#minimum_release_age"
|
|
15
16
|
MIN_BIN = (2026, 4, 22)
|
|
17
|
+
EXTRAS = [
|
|
18
|
+
{"key": "paranoid", "expected": "true", "line": "paranoid = true", "section": SECTION},
|
|
19
|
+
]
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
def preflight() -> dict:
|
|
@@ -59,13 +63,14 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
59
63
|
p = path(env, home, platform)
|
|
60
64
|
raw = p.read_text("utf-8") if p.exists() else ""
|
|
61
65
|
value = read_key(raw, KEY, section=SECTION)
|
|
62
|
-
return {"path": str(p), "configured": value, "days": _parse_days(value)}
|
|
66
|
+
return {"path": str(p), "configured": value, "days": _parse_days(value), "extras": read_extras(raw, EXTRAS)}
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
66
70
|
p = path(env, home, platform)
|
|
67
71
|
before = p.read_text("utf-8") if p.exists() else ""
|
|
68
72
|
after = set_key(before, KEY, f'{KEY} = "{days}d"', section=SECTION)
|
|
73
|
+
after = apply_extras(after, EXTRAS)
|
|
69
74
|
write_atomic(p, after)
|
|
70
75
|
return {"path": str(p), "before": before, "after": after}
|
|
71
76
|
|
|
@@ -75,7 +80,9 @@ def unset(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
75
80
|
if not p.exists():
|
|
76
81
|
return {"path": str(p), "removed": False}
|
|
77
82
|
before = p.read_text("utf-8")
|
|
78
|
-
after,
|
|
83
|
+
after, removed_cooldown = remove_key(before, KEY, section=SECTION)
|
|
84
|
+
after, removed_extras = remove_extras(after, EXTRAS)
|
|
85
|
+
removed = removed_cooldown or removed_extras
|
|
79
86
|
if removed:
|
|
80
87
|
write_atomic(p, after)
|
|
81
88
|
return {"path": str(p), "removed": removed}
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from pmsec.util.extras import apply_extras, read_extras, remove_extras
|
|
5
6
|
from pmsec.util.io import write_atomic
|
|
6
7
|
from pmsec.util.lines import read_key, remove_key, set_key
|
|
7
8
|
from pmsec.util.paths import npmrc_path
|
|
@@ -11,6 +12,9 @@ NAME = "npm"
|
|
|
11
12
|
KEY = "min-release-age"
|
|
12
13
|
DOCS = "https://docs.npmjs.com/cli/v11/using-npm/config#min-release-age"
|
|
13
14
|
MIN_BIN = (11, 10, 0)
|
|
15
|
+
EXTRAS = [
|
|
16
|
+
{"key": "audit-level", "expected": "high", "line": "audit-level=high"},
|
|
17
|
+
]
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
def preflight() -> dict:
|
|
@@ -35,13 +39,14 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
35
39
|
raw = p.read_text("utf-8") if p.exists() else ""
|
|
36
40
|
value = read_key(raw, KEY)
|
|
37
41
|
days = None if value is None else int(value)
|
|
38
|
-
return {"path": str(p), "configured": value, "days": days}
|
|
42
|
+
return {"path": str(p), "configured": value, "days": days, "extras": read_extras(raw, EXTRAS)}
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
42
46
|
p = path(env, home, platform)
|
|
43
47
|
before = p.read_text("utf-8") if p.exists() else ""
|
|
44
48
|
after = set_key(before, KEY, f"{KEY}={days}")
|
|
49
|
+
after = apply_extras(after, EXTRAS)
|
|
45
50
|
write_atomic(p, after)
|
|
46
51
|
return {"path": str(p), "before": before, "after": after}
|
|
47
52
|
|
|
@@ -51,7 +56,9 @@ def unset(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
51
56
|
if not p.exists():
|
|
52
57
|
return {"path": str(p), "removed": False}
|
|
53
58
|
before = p.read_text("utf-8")
|
|
54
|
-
after,
|
|
59
|
+
after, removed_cooldown = remove_key(before, KEY)
|
|
60
|
+
after, removed_extras = remove_extras(after, EXTRAS)
|
|
61
|
+
removed = removed_cooldown or removed_extras
|
|
55
62
|
if removed:
|
|
56
63
|
write_atomic(p, after)
|
|
57
64
|
return {"path": str(p), "removed": removed}
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from pmsec.util.extras import apply_extras, read_extras, remove_extras
|
|
5
6
|
from pmsec.util.io import write_atomic
|
|
6
7
|
from pmsec.util.lines import read_key, remove_key, set_key
|
|
7
8
|
from pmsec.util.paths import npmrc_path
|
|
@@ -11,6 +12,10 @@ NAME = "pnpm"
|
|
|
11
12
|
KEY = "minimum-release-age"
|
|
12
13
|
DOCS = "https://pnpm.io/settings#minimumreleaseage"
|
|
13
14
|
MIN_BIN = (10, 6, 0)
|
|
15
|
+
EXTRAS = [
|
|
16
|
+
{"key": "trust-policy", "expected": "no-downgrade", "line": "trust-policy=no-downgrade"},
|
|
17
|
+
{"key": "block-exotic-subdeps", "expected": "true", "line": "block-exotic-subdeps=true"},
|
|
18
|
+
]
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
def path(env: dict[str, str], home: Path, platform: str) -> Path:
|
|
@@ -41,13 +46,14 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
41
46
|
except ValueError:
|
|
42
47
|
minutes = None
|
|
43
48
|
days = None if minutes is None else minutes // (60 * 24)
|
|
44
|
-
return {"path": str(p), "configured": value, "days": days}
|
|
49
|
+
return {"path": str(p), "configured": value, "days": days, "extras": read_extras(raw, EXTRAS)}
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
48
53
|
p = path(env, home, platform)
|
|
49
54
|
before = p.read_text("utf-8") if p.exists() else ""
|
|
50
55
|
after = set_key(before, KEY, f"{KEY}={days * 24 * 60}")
|
|
56
|
+
after = apply_extras(after, EXTRAS)
|
|
51
57
|
write_atomic(p, after)
|
|
52
58
|
return {"path": str(p), "before": before, "after": after}
|
|
53
59
|
|
|
@@ -57,7 +63,9 @@ def unset(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
57
63
|
if not p.exists():
|
|
58
64
|
return {"path": str(p), "removed": False}
|
|
59
65
|
before = p.read_text("utf-8")
|
|
60
|
-
after,
|
|
66
|
+
after, removed_cooldown = remove_key(before, KEY)
|
|
67
|
+
after, removed_extras = remove_extras(after, EXTRAS)
|
|
68
|
+
removed = removed_cooldown or removed_extras
|
|
61
69
|
if removed:
|
|
62
70
|
write_atomic(p, after)
|
|
63
71
|
return {"path": str(p), "removed": removed}
|
|
@@ -12,6 +12,7 @@ NAME = "uv"
|
|
|
12
12
|
KEY = "exclude-newer"
|
|
13
13
|
DOCS = "https://docs.astral.sh/uv/reference/settings/#exclude-newer"
|
|
14
14
|
MIN_BIN = (0, 9, 17)
|
|
15
|
+
EXTRAS: list[dict] = []
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def preflight() -> dict:
|
|
@@ -48,7 +49,7 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
48
49
|
p = path(env, home, platform)
|
|
49
50
|
raw = p.read_text("utf-8") if p.exists() else ""
|
|
50
51
|
value = read_key(raw, KEY)
|
|
51
|
-
return {"path": str(p), "configured": value, "days": _parse_days(value)}
|
|
52
|
+
return {"path": str(p), "configured": value, "days": _parse_days(value), "extras": []}
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from pmsec.util.extras import apply_extras, read_extras, remove_extras
|
|
6
7
|
from pmsec.util.io import write_atomic
|
|
7
8
|
from pmsec.util.lines import read_key, remove_key, set_key
|
|
8
9
|
from pmsec.util.paths import yarnrc_path
|
|
@@ -13,6 +14,9 @@ KEY = "npmMinimalAgeGate"
|
|
|
13
14
|
DOCS = "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate"
|
|
14
15
|
MIN_BIN = (4, 10, 0)
|
|
15
16
|
SEP = ":"
|
|
17
|
+
EXTRAS = [
|
|
18
|
+
{"key": "enableHardenedMode", "expected": "true", "line": "enableHardenedMode: true", "sep": SEP},
|
|
19
|
+
]
|
|
16
20
|
|
|
17
21
|
_DURATION = re.compile(r'^"?\s*(\d+)\s*(d|days?|w|weeks?)\s*"?$', re.IGNORECASE)
|
|
18
22
|
|
|
@@ -48,13 +52,14 @@ def read(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
48
52
|
p = path(env, home, platform)
|
|
49
53
|
raw = p.read_text("utf-8") if p.exists() else ""
|
|
50
54
|
value = read_key(raw, KEY, sep=SEP)
|
|
51
|
-
return {"path": str(p), "configured": value, "days": _parse_days(value)}
|
|
55
|
+
return {"path": str(p), "configured": value, "days": _parse_days(value), "extras": read_extras(raw, EXTRAS)}
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
def write(days: int, env: dict[str, str], home: Path, platform: str) -> dict:
|
|
55
59
|
p = path(env, home, platform)
|
|
56
60
|
before = p.read_text("utf-8") if p.exists() else ""
|
|
57
61
|
after = set_key(before, KEY, f'{KEY}: "{days}d"', sep=SEP)
|
|
62
|
+
after = apply_extras(after, EXTRAS)
|
|
58
63
|
write_atomic(p, after)
|
|
59
64
|
return {"path": str(p), "before": before, "after": after}
|
|
60
65
|
|
|
@@ -64,7 +69,9 @@ def unset(env: dict[str, str], home: Path, platform: str) -> dict:
|
|
|
64
69
|
if not p.exists():
|
|
65
70
|
return {"path": str(p), "removed": False}
|
|
66
71
|
before = p.read_text("utf-8")
|
|
67
|
-
after,
|
|
72
|
+
after, removed_cooldown = remove_key(before, KEY, sep=SEP)
|
|
73
|
+
after, removed_extras = remove_extras(after, EXTRAS)
|
|
74
|
+
removed = removed_cooldown or removed_extras
|
|
68
75
|
if removed:
|
|
69
76
|
write_atomic(p, after)
|
|
70
77
|
return {"path": str(p), "removed": removed}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pmsec.util.lines import read_key, remove_key, set_key
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_extras(raw: str, extras: list[dict]) -> list[dict]:
|
|
7
|
+
rows = []
|
|
8
|
+
for e in extras:
|
|
9
|
+
cur = read_key(raw, e["key"], sep=e.get("sep", "="), section=e.get("section"))
|
|
10
|
+
rows.append({
|
|
11
|
+
"key": e["key"],
|
|
12
|
+
"configured": cur,
|
|
13
|
+
"expected": e["expected"],
|
|
14
|
+
"ok": cur == e["expected"],
|
|
15
|
+
})
|
|
16
|
+
return rows
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def apply_extras(text: str, extras: list[dict]) -> str:
|
|
20
|
+
out = text
|
|
21
|
+
for e in extras:
|
|
22
|
+
out = set_key(out, e["key"], e["line"], sep=e.get("sep", "="), section=e.get("section"))
|
|
23
|
+
return out
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove_extras(text: str, extras: list[dict]) -> tuple[str, bool]:
|
|
27
|
+
out = text
|
|
28
|
+
removed = False
|
|
29
|
+
for e in extras:
|
|
30
|
+
out, r = remove_key(out, e["key"], sep=e.get("sep", "="), section=e.get("section"))
|
|
31
|
+
if r:
|
|
32
|
+
removed = True
|
|
33
|
+
return out, removed
|
|
@@ -35,6 +35,12 @@ def test_set_writes_every_supported_tool_config(tmp_path):
|
|
|
35
35
|
mise = (tmp_path / ".config" / "mise" / "config.toml").read_text()
|
|
36
36
|
assert "[settings]" in mise
|
|
37
37
|
assert 'minimum_release_age = "7d"' in mise
|
|
38
|
+
assert "paranoid = true" in mise
|
|
39
|
+
npmrc = (tmp_path / ".npmrc").read_text()
|
|
40
|
+
assert "audit-level=high" in npmrc
|
|
41
|
+
assert "trust-policy=no-downgrade" in npmrc
|
|
42
|
+
assert "block-exotic-subdeps=true" in npmrc
|
|
43
|
+
assert "enableHardenedMode: true" in (tmp_path / ".yarnrc.yml").read_text()
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
def test_check_passes_after_set(tmp_path):
|
|
@@ -75,7 +81,9 @@ def test_unset_preserves_other_keys(tmp_path):
|
|
|
75
81
|
def test_set_replaces_existing_value(tmp_path):
|
|
76
82
|
(tmp_path / ".npmrc").write_text("min-release-age=3\nregistry=https://r/\n")
|
|
77
83
|
run(["set", "10", "--tool", "npm"], tmp_path)
|
|
78
|
-
assert (tmp_path / ".npmrc").read_text() ==
|
|
84
|
+
assert (tmp_path / ".npmrc").read_text() == (
|
|
85
|
+
"min-release-age=10\nregistry=https://r/\naudit-level=high\n"
|
|
86
|
+
)
|
|
79
87
|
|
|
80
88
|
|
|
81
89
|
def test_tool_filter(tmp_path):
|
|
@@ -101,8 +109,9 @@ def test_check_json(tmp_path):
|
|
|
101
109
|
|
|
102
110
|
|
|
103
111
|
def test_set_rejects_zero(tmp_path):
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
code, _, err = run(["set", "0"], tmp_path)
|
|
113
|
+
assert code == 2
|
|
114
|
+
assert "set requires integer DAYS > 0" in err
|
|
106
115
|
|
|
107
116
|
|
|
108
117
|
def test_bun_section_insert(tmp_path):
|
|
@@ -120,7 +129,9 @@ def test_bun_creates_section_if_missing(tmp_path):
|
|
|
120
129
|
|
|
121
130
|
|
|
122
131
|
def test_yarn_check_parses_days(tmp_path):
|
|
123
|
-
(tmp_path / ".yarnrc.yml").write_text(
|
|
132
|
+
(tmp_path / ".yarnrc.yml").write_text(
|
|
133
|
+
'npmMinimalAgeGate: "14d"\nenableHardenedMode: true\n'
|
|
134
|
+
)
|
|
124
135
|
_, out, _ = run(["check", "--json", "--tool", "yarn", "--min", "7"], tmp_path)
|
|
125
136
|
data = json.loads(out)
|
|
126
137
|
assert data["ok"] is True
|
|
@@ -139,3 +150,37 @@ def test_bak_created_once(tmp_path):
|
|
|
139
150
|
run(["set", "7", "--tool", "npm"], tmp_path)
|
|
140
151
|
run(["set", "10", "--tool", "npm"], tmp_path)
|
|
141
152
|
assert (tmp_path / ".npmrc.bak").read_text() == "registry=https://original/\n"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_hardening_extras_roundtrip(tmp_path):
|
|
156
|
+
(tmp_path / ".npmrc").write_text("minimum-release-age=20160\n")
|
|
157
|
+
code, out, _ = run(["check", "--json", "--tool", "pnpm", "--min", "7"], tmp_path)
|
|
158
|
+
data = json.loads(out)
|
|
159
|
+
assert code == 1
|
|
160
|
+
assert len(data["rows"][0]["extras"]) == 2
|
|
161
|
+
assert all(not e["ok"] for e in data["rows"][0]["extras"])
|
|
162
|
+
|
|
163
|
+
run(["set", "14", "--tool", "pnpm"], tmp_path)
|
|
164
|
+
npmrc = (tmp_path / ".npmrc").read_text()
|
|
165
|
+
assert "trust-policy=no-downgrade" in npmrc
|
|
166
|
+
assert "block-exotic-subdeps=true" in npmrc
|
|
167
|
+
|
|
168
|
+
code, out, _ = run(["check", "--json", "--tool", "pnpm", "--min", "7"], tmp_path)
|
|
169
|
+
assert code == 0
|
|
170
|
+
assert json.loads(out)["ok"] is True
|
|
171
|
+
|
|
172
|
+
run(["unset", "--tool", "pnpm"], tmp_path)
|
|
173
|
+
after = (tmp_path / ".npmrc").read_text()
|
|
174
|
+
assert "trust-policy" not in after
|
|
175
|
+
assert "block-exotic-subdeps" not in after
|
|
176
|
+
assert "minimum-release-age" not in after
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.parametrize("flag", ["--version", "-V"])
|
|
180
|
+
def test_version_flag_prints_package_version(tmp_path, capsys, flag):
|
|
181
|
+
from pmsec import __version__
|
|
182
|
+
with pytest.raises(SystemExit) as exc:
|
|
183
|
+
run([flag], tmp_path)
|
|
184
|
+
assert exc.value.code == 0
|
|
185
|
+
captured = capsys.readouterr()
|
|
186
|
+
assert captured.out.strip() == f"pmsec {__version__}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__all__ = ["cli"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|