doclify-guardrail 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/package.json +30 -0
- package/src/checker.mjs +206 -0
- package/src/ci-output.mjs +247 -0
- package/src/colors.mjs +74 -0
- package/src/fixer.mjs +39 -0
- package/src/glob.mjs +124 -0
- package/src/index.mjs +480 -0
- package/src/links.mjs +118 -0
- package/src/quality.mjs +94 -0
- package/src/report.mjs +83 -0
- package/src/rules-loader.mjs +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lorenzo Borgato
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Doclify Guardrail
|
|
2
|
+
|
|
3
|
+
**Quality gate for your Markdown docs. Catches errors in seconds.**
|
|
4
|
+
|
|
5
|
+
Zero dependencies. Node.js built-in only. Works everywhere Node 20+ runs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Multi-file and directory scanning with glob support
|
|
10
|
+
- Line numbers in every finding
|
|
11
|
+
- Code block exclusion (no false positives on code examples)
|
|
12
|
+
- Markdown report generation for CI
|
|
13
|
+
- Custom regex-based rules via JSON
|
|
14
|
+
- Colored terminal output
|
|
15
|
+
- Extended placeholder detection (TODO, FIXME, TBD, WIP, and more)
|
|
16
|
+
- Insecure link detection (inline, bare URLs, reference-style)
|
|
17
|
+
- Doc Health Score per file and project average (0–100)
|
|
18
|
+
- Optional dead link checker (`--check-links`)
|
|
19
|
+
- Optional freshness checker (`--check-freshness`)
|
|
20
|
+
- CI-ready output exports (`--junit`, `--sarif`)
|
|
21
|
+
- Local docs health badge generation (`--badge`, `--badge-label`)
|
|
22
|
+
- Optional safe auto-fix mode (`--fix`, `--dry-run`)
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Scan a single file
|
|
28
|
+
npx doclify-guardrail README.md
|
|
29
|
+
|
|
30
|
+
# Scan an entire directory
|
|
31
|
+
npx doclify-guardrail docs/
|
|
32
|
+
|
|
33
|
+
# Strict mode (warnings = failure)
|
|
34
|
+
npx doclify-guardrail docs/ --strict
|
|
35
|
+
|
|
36
|
+
# Generate a report
|
|
37
|
+
npx doclify-guardrail docs/ --report
|
|
38
|
+
|
|
39
|
+
# Check dead links (HTTP status + local relative paths)
|
|
40
|
+
npx doclify-guardrail docs/ --check-links
|
|
41
|
+
|
|
42
|
+
# Check document freshness (warn if stale or missing updated date)
|
|
43
|
+
npx doclify-guardrail docs/ --check-freshness
|
|
44
|
+
|
|
45
|
+
# Export CI reports
|
|
46
|
+
npx doclify-guardrail docs/ --junit --sarif
|
|
47
|
+
|
|
48
|
+
# Generate local badge SVG
|
|
49
|
+
npx doclify-guardrail docs/ --badge --badge-label "docs health"
|
|
50
|
+
|
|
51
|
+
# Auto-fix safe issues (v1: http:// -> https://)
|
|
52
|
+
npx doclify-guardrail docs/ --fix
|
|
53
|
+
|
|
54
|
+
# Preview auto-fix without writing files
|
|
55
|
+
npx doclify-guardrail docs/ --fix --dry-run
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
doclify-guardrail <file.md ...> [options]
|
|
62
|
+
doclify-guardrail --dir <path> [options]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Options
|
|
66
|
+
|
|
67
|
+
| Flag | Description |
|
|
68
|
+
|------|-------------|
|
|
69
|
+
| `--strict` | Treat warnings as failures |
|
|
70
|
+
| `--max-line-length <n>` | Maximum line length (default: 160) |
|
|
71
|
+
| `--config <path>` | Config file path (default: `.doclify-guardrail.json`) |
|
|
72
|
+
| `--dir <path>` | Scan all `.md` files in directory (recursive) |
|
|
73
|
+
| `--report [path]` | Generate markdown report (default: `doclify-report.md`) |
|
|
74
|
+
| `--rules <path>` | Load custom rules from JSON file |
|
|
75
|
+
| `--check-links` | Validate links and fail on dead links |
|
|
76
|
+
| `--check-freshness` | Warn if docs are stale or missing an updated date (default max age: 180 days) |
|
|
77
|
+
| `--junit [path]` | Export JUnit XML report (default: `doclify-junit.xml`) |
|
|
78
|
+
| `--sarif [path]` | Export SARIF report (default: `doclify.sarif`) |
|
|
79
|
+
| `--badge [path]` | Generate SVG docs health badge (default: `doclify-badge.svg`) |
|
|
80
|
+
| `--badge-label <text>` | Custom label used in the generated badge |
|
|
81
|
+
| `--fix` | Auto-fix safe issues (v1: `http://` to `https://`) |
|
|
82
|
+
| `--dry-run` | Preview `--fix` changes without writing files (only valid with `--fix`) |
|
|
83
|
+
| `--no-color` | Disable colored output |
|
|
84
|
+
| `--debug` | Show runtime details |
|
|
85
|
+
| `-h, --help` | Show help |
|
|
86
|
+
|
|
87
|
+
### Exit Codes
|
|
88
|
+
|
|
89
|
+
| Code | Meaning |
|
|
90
|
+
|------|---------|
|
|
91
|
+
| `0` | PASS -- all files clean |
|
|
92
|
+
| `1` | FAIL -- errors found, or warnings in strict mode |
|
|
93
|
+
| `2` | Usage error / invalid input |
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Create a `.doclify-guardrail.json` in your project root:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"maxLineLength": 120,
|
|
102
|
+
"strict": true
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
CLI flags override config file values.
|
|
107
|
+
|
|
108
|
+
## Built-in Rules
|
|
109
|
+
|
|
110
|
+
| Rule | Severity | Description |
|
|
111
|
+
|------|----------|-------------|
|
|
112
|
+
| `frontmatter` | warning | Missing YAML frontmatter block |
|
|
113
|
+
| `single-h1` | error | Zero or multiple H1 headings |
|
|
114
|
+
| `line-length` | warning | Lines exceeding max length |
|
|
115
|
+
| `placeholder` | warning | TODO, FIXME, TBD, WIP, HACK, CHANGEME, lorem ipsum, etc. |
|
|
116
|
+
| `insecure-link` | warning | HTTP links (should be HTTPS) |
|
|
117
|
+
| `dead-link` | error | Broken links (enabled with `--check-links`) |
|
|
118
|
+
| `stale-doc` | warning | Missing/old freshness date (enabled with `--check-freshness`) |
|
|
119
|
+
|
|
120
|
+
All rules respect code block exclusion -- content inside fenced code blocks
|
|
121
|
+
and inline code is never flagged.
|
|
122
|
+
|
|
123
|
+
## Custom Rules
|
|
124
|
+
|
|
125
|
+
Create a JSON file with custom regex-based rules:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"rules": [
|
|
130
|
+
{
|
|
131
|
+
"id": "no-internal-urls",
|
|
132
|
+
"severity": "error",
|
|
133
|
+
"pattern": "https://internal\\.company\\.com",
|
|
134
|
+
"message": "Internal URL found -- remove before publishing"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "no-draft-marker",
|
|
138
|
+
"severity": "warning",
|
|
139
|
+
"pattern": "\\[DRAFT\\]",
|
|
140
|
+
"message": "Draft marker found in document"
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
doclify-guardrail docs/ --rules my-rules.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Custom rules are applied after built-in rules and respect code block exclusion.
|
|
151
|
+
|
|
152
|
+
## CI Integration
|
|
153
|
+
|
|
154
|
+
### GitHub Actions
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
- name: Docs quality gate
|
|
158
|
+
run: npx doclify-guardrail docs/ --strict --report
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
See `.github/workflows/docs-check.yml` for a complete example workflow.
|
|
162
|
+
|
|
163
|
+
### JSON Output
|
|
164
|
+
|
|
165
|
+
Pipe JSON output to other tools:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
doclify-guardrail docs/ 2>/dev/null | jq '.summary'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Dead Link Checker
|
|
172
|
+
|
|
173
|
+
Use `--check-links` to validate:
|
|
174
|
+
- `http(s)` links via HTTP status checks
|
|
175
|
+
- relative local file links (e.g. `./guide.md`)
|
|
176
|
+
|
|
177
|
+
When dead links are found, they are reported as `dead-link` errors.
|
|
178
|
+
|
|
179
|
+
## CI Outputs
|
|
180
|
+
|
|
181
|
+
Use CI export flags for native pipeline integrations:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
doclify-guardrail docs/ --junit --sarif
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- `--junit` writes XML test output (useful for generic CI test dashboards)
|
|
188
|
+
- `--sarif` writes SARIF v2.1.0 (GitHub code scanning compatible)
|
|
189
|
+
|
|
190
|
+
## Badge SVG
|
|
191
|
+
|
|
192
|
+
Generate a local SVG badge from the current scan score:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
doclify-guardrail docs/ --badge --badge-label "docs health"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The value is computed from findings severity and can be embedded in your README.
|
|
199
|
+
|
|
200
|
+
## Auto-fix (safe v1)
|
|
201
|
+
|
|
202
|
+
Use `--fix` to automatically upgrade safe `http://` links to `https://`.
|
|
203
|
+
Ambiguous URLs (for example `localhost` or custom ports) are reported and left unchanged.
|
|
204
|
+
|
|
205
|
+
Use `--dry-run` only together with `--fix` to preview changes without writing files.
|
|
206
|
+
Using `--dry-run` alone is a usage error (exit code `2`).
|
|
207
|
+
|
|
208
|
+
## Doc Health Score
|
|
209
|
+
|
|
210
|
+
Each file now includes `summary.healthScore` (0–100) in JSON output.
|
|
211
|
+
Overall summary includes `summary.avgHealthScore`.
|
|
212
|
+
|
|
213
|
+
Scoring is deterministic and compatibility-safe:
|
|
214
|
+
- starts at `100`
|
|
215
|
+
- `-25` per error
|
|
216
|
+
- `-8` per warning
|
|
217
|
+
- clamped to `0..100`
|
|
218
|
+
|
|
219
|
+
## Doc Freshness
|
|
220
|
+
|
|
221
|
+
Use `--check-freshness` to detect stale docs.
|
|
222
|
+
The checker looks for dates in:
|
|
223
|
+
- frontmatter keys: `updated`, `last_updated`, `lastModified`, `date`
|
|
224
|
+
- body marker: `Last updated: YYYY-MM-DD`
|
|
225
|
+
|
|
226
|
+
If no date is found or the doc is older than 180 days, a `stale-doc` warning is added.
|
|
227
|
+
|
|
228
|
+
## Report
|
|
229
|
+
|
|
230
|
+
Use `--report` to generate a markdown report:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
doclify-guardrail docs/ --report quality-report.md
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The report includes a summary table, per-file details with line numbers,
|
|
237
|
+
and execution metadata.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Italiano
|
|
246
|
+
|
|
247
|
+
Doclify Guardrail e' un quality gate per la documentazione Markdown.
|
|
248
|
+
Zero dipendenze esterne, funziona ovunque giri Node.js 20+.
|
|
249
|
+
Rileva errori, placeholder dimenticati, link insicuri e problemi di
|
|
250
|
+
formattazione con numeri di riga precisi e report in formato Markdown.
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doclify-guardrail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Quality gate for your Markdown docs. Zero dependencies, catches errors in seconds.",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"doclify-guardrail": "./src/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node ./src/index.mjs",
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"demo": "bash ./scripts/demo.sh"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"markdown",
|
|
24
|
+
"lint",
|
|
25
|
+
"documentation",
|
|
26
|
+
"quality",
|
|
27
|
+
"guardrail",
|
|
28
|
+
"cli"
|
|
29
|
+
]
|
|
30
|
+
}
|
package/src/checker.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
maxLineLength: 160,
|
|
3
|
+
strict: false
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const RULE_SEVERITY = {
|
|
7
|
+
frontmatter: 'warning',
|
|
8
|
+
'single-h1': 'error',
|
|
9
|
+
'line-length': 'warning',
|
|
10
|
+
placeholder: 'warning',
|
|
11
|
+
'insecure-link': 'warning',
|
|
12
|
+
'dead-link': 'error',
|
|
13
|
+
'stale-doc': 'warning'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const PLACEHOLDER_PATTERNS = [
|
|
17
|
+
{ rx: /\bTODO\b/i, msg: 'TODO marker found — remove before publishing' },
|
|
18
|
+
{ rx: /\bFIXME\b/i, msg: 'FIXME marker found — remove before publishing' },
|
|
19
|
+
{ rx: /\bHACK\b/i, msg: 'HACK marker found — remove before publishing' },
|
|
20
|
+
{ rx: /\bTBD\b/i, msg: 'TBD (to be determined) marker found' },
|
|
21
|
+
{ rx: /\bWIP\b/i, msg: 'WIP (work in progress) marker found' },
|
|
22
|
+
{ rx: /\bCHANGEME\b/i, msg: 'CHANGEME marker found — update before publishing' },
|
|
23
|
+
{ rx: /\bPLACEHOLDER\b/i, msg: 'PLACEHOLDER marker found — replace with actual content' },
|
|
24
|
+
{ rx: /\[insert\s+here\]/i, msg: '"[insert here]" placeholder found' },
|
|
25
|
+
{ rx: /lorem ipsum/i, msg: 'Lorem ipsum placeholder text found' },
|
|
26
|
+
{ rx: /\bxxx\b/i, msg: '"xxx" placeholder found' }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function normalizeFinding(rule, message, line, source) {
|
|
30
|
+
const finding = {
|
|
31
|
+
code: rule,
|
|
32
|
+
severity: RULE_SEVERITY[rule] || 'warning',
|
|
33
|
+
message
|
|
34
|
+
};
|
|
35
|
+
if (line != null) finding.line = line;
|
|
36
|
+
if (source != null) finding.source = source;
|
|
37
|
+
return finding;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Replace content inside fenced code blocks with empty lines.
|
|
42
|
+
* Preserves line count so line numbers remain accurate.
|
|
43
|
+
*/
|
|
44
|
+
function stripCodeBlocks(content) {
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
const result = [];
|
|
47
|
+
let inCodeBlock = false;
|
|
48
|
+
let fenceChar = null;
|
|
49
|
+
let fenceLen = 0;
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
if (!inCodeBlock) {
|
|
53
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
54
|
+
if (fenceMatch) {
|
|
55
|
+
inCodeBlock = true;
|
|
56
|
+
fenceChar = fenceMatch[1][0];
|
|
57
|
+
fenceLen = fenceMatch[1].length;
|
|
58
|
+
result.push('');
|
|
59
|
+
} else {
|
|
60
|
+
result.push(line);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
64
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLen) {
|
|
65
|
+
inCodeBlock = false;
|
|
66
|
+
fenceChar = null;
|
|
67
|
+
fenceLen = 0;
|
|
68
|
+
}
|
|
69
|
+
result.push('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip inline code from a single line for rule matching.
|
|
78
|
+
*/
|
|
79
|
+
function stripInlineCode(line) {
|
|
80
|
+
return line.replace(/`[^`]+`/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkMarkdown(rawContent, opts = {}) {
|
|
84
|
+
const maxLineLength = Number(opts.maxLineLength ?? DEFAULTS.maxLineLength);
|
|
85
|
+
const filePath = opts.filePath || undefined;
|
|
86
|
+
const errors = [];
|
|
87
|
+
const warnings = [];
|
|
88
|
+
|
|
89
|
+
// Strip code blocks for semantic rules
|
|
90
|
+
const content = stripCodeBlocks(rawContent);
|
|
91
|
+
const lines = content.split('\n');
|
|
92
|
+
const rawLines = rawContent.split('\n');
|
|
93
|
+
|
|
94
|
+
// Rule: frontmatter
|
|
95
|
+
if (!rawContent.startsWith('---\n')) {
|
|
96
|
+
warnings.push(normalizeFinding('frontmatter', 'Missing frontmatter block at the beginning of the file.', 1, filePath));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Rule: single-h1
|
|
100
|
+
const h1Lines = [];
|
|
101
|
+
lines.forEach((line, idx) => {
|
|
102
|
+
if (/^#\s/.test(line)) {
|
|
103
|
+
h1Lines.push(idx + 1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (h1Lines.length === 0) {
|
|
108
|
+
errors.push(normalizeFinding('single-h1', 'Missing H1 heading.', 1, filePath));
|
|
109
|
+
} else if (h1Lines.length > 1) {
|
|
110
|
+
const lineList = h1Lines.join(', ');
|
|
111
|
+
for (const lineNum of h1Lines) {
|
|
112
|
+
errors.push(normalizeFinding(
|
|
113
|
+
'single-h1',
|
|
114
|
+
`Found ${h1Lines.length} H1 headings (expected 1) at lines ${lineList}.`,
|
|
115
|
+
lineNum,
|
|
116
|
+
filePath
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Rule: line-length (uses raw content — code block lines can still be too long)
|
|
122
|
+
rawLines.forEach((line, idx) => {
|
|
123
|
+
if (line.length > maxLineLength) {
|
|
124
|
+
warnings.push(
|
|
125
|
+
normalizeFinding(
|
|
126
|
+
'line-length',
|
|
127
|
+
`Line exceeds ${maxLineLength} characters (${line.length}).`,
|
|
128
|
+
idx + 1,
|
|
129
|
+
filePath
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Rule: placeholder (uses stripped content)
|
|
136
|
+
lines.forEach((line, idx) => {
|
|
137
|
+
const cleanLine = stripInlineCode(line);
|
|
138
|
+
for (const { rx, msg } of PLACEHOLDER_PATTERNS) {
|
|
139
|
+
if (rx.test(cleanLine)) {
|
|
140
|
+
warnings.push(normalizeFinding('placeholder', msg, idx + 1, filePath));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Rule: insecure-link (uses stripped content)
|
|
146
|
+
lines.forEach((line, idx) => {
|
|
147
|
+
const cleanLine = stripInlineCode(line);
|
|
148
|
+
|
|
149
|
+
// Inline markdown links: [text](http://...)
|
|
150
|
+
const inlineMatches = cleanLine.match(/\[.*?\]\(http:\/\/[^)]+\)/g);
|
|
151
|
+
if (inlineMatches) {
|
|
152
|
+
for (const match of inlineMatches) {
|
|
153
|
+
const url = match.match(/\((http:\/\/[^)]+)\)/)?.[1] || '';
|
|
154
|
+
warnings.push(
|
|
155
|
+
normalizeFinding('insecure-link', `Insecure link found: ${url} — use https:// instead`, idx + 1, filePath)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Reference-style link definitions: [label]: http://...
|
|
161
|
+
const refMatch = cleanLine.match(/^\[.*?\]:\s*(http:\/\/\S+)/);
|
|
162
|
+
if (refMatch) {
|
|
163
|
+
warnings.push(
|
|
164
|
+
normalizeFinding('insecure-link', `Insecure link found: ${refMatch[1]} — use https:// instead`, idx + 1, filePath)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Bare URLs: http://... (not inside markdown link syntax)
|
|
169
|
+
// Only check if no inline links were found on this line to avoid duplicates
|
|
170
|
+
if (!inlineMatches && !refMatch) {
|
|
171
|
+
const bareMatch = cleanLine.match(/\bhttp:\/\/\S+/g);
|
|
172
|
+
if (bareMatch) {
|
|
173
|
+
for (const url of bareMatch) {
|
|
174
|
+
warnings.push(
|
|
175
|
+
normalizeFinding('insecure-link', `Insecure link found: ${url} — use https:// instead`, idx + 1, filePath)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Custom rules (uses stripped content)
|
|
183
|
+
if (opts.customRules && opts.customRules.length > 0) {
|
|
184
|
+
lines.forEach((line, idx) => {
|
|
185
|
+
const cleanLine = stripInlineCode(line);
|
|
186
|
+
for (const rule of opts.customRules) {
|
|
187
|
+
rule.pattern.lastIndex = 0;
|
|
188
|
+
if (rule.pattern.test(cleanLine)) {
|
|
189
|
+
const bucket = rule.severity === 'error' ? errors : warnings;
|
|
190
|
+
bucket.push(normalizeFinding(rule.id, rule.message, idx + 1, filePath));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
errors,
|
|
198
|
+
warnings,
|
|
199
|
+
summary: {
|
|
200
|
+
errors: errors.length,
|
|
201
|
+
warnings: warnings.length
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { DEFAULTS, RULE_SEVERITY, normalizeFinding, checkMarkdown, stripCodeBlocks, stripInlineCode };
|