@frozenproductions/niteo 0.1.0 → 0.1.1
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/README.md +33 -2
- package/package.json +1 -1
- package/src/app.rs +34 -3
- package/src/cli.rs +16 -1
- package/src/report.rs +150 -0
package/README.md
CHANGED
|
@@ -23,13 +23,20 @@ Exact rules, configuration options, and report details are still changing during
|
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Run directly with `npx`:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
29
|
npx @frozenproductions/niteo lint
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Or install globally and use the `niteo` command:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm i -g @frozenproductions/niteo
|
|
36
|
+
niteo lint
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The npm package builds the Rust binary during installation, so Rust and Cargo must be installed on the machine running `npx` or `npm i -g`.
|
|
33
40
|
|
|
34
41
|
For local development, run it from source:
|
|
35
42
|
|
|
@@ -55,30 +62,52 @@ Scan the default project root:
|
|
|
55
62
|
|
|
56
63
|
```sh
|
|
57
64
|
npx @frozenproductions/niteo lint
|
|
65
|
+
# or, after npm i -g @frozenproductions/niteo
|
|
66
|
+
niteo lint
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
Generate a starter config:
|
|
61
70
|
|
|
62
71
|
```sh
|
|
63
72
|
npx @frozenproductions/niteo init
|
|
73
|
+
# or
|
|
74
|
+
niteo init
|
|
64
75
|
```
|
|
65
76
|
|
|
66
77
|
Scan a specific root:
|
|
67
78
|
|
|
68
79
|
```sh
|
|
69
80
|
npx @frozenproductions/niteo lint --root src
|
|
81
|
+
# or
|
|
82
|
+
niteo lint --root src
|
|
70
83
|
```
|
|
71
84
|
|
|
72
85
|
Restrict the scan to a path:
|
|
73
86
|
|
|
74
87
|
```sh
|
|
75
88
|
npx @frozenproductions/niteo lint --scope src/components
|
|
89
|
+
# or
|
|
90
|
+
niteo lint --scope src/components
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Write JSON output to a file:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
niteo lint --format json --output niteo-report.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Write SARIF output for code scanning tools:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
niteo lint --format sarif --output niteo-report.sarif
|
|
76
103
|
```
|
|
77
104
|
|
|
78
105
|
Show help:
|
|
79
106
|
|
|
80
107
|
```sh
|
|
81
108
|
npx @frozenproductions/niteo --help
|
|
109
|
+
# or
|
|
110
|
+
niteo --help
|
|
82
111
|
```
|
|
83
112
|
|
|
84
113
|
## Configuration
|
|
@@ -89,6 +118,8 @@ You can generate a starter config with:
|
|
|
89
118
|
|
|
90
119
|
```sh
|
|
91
120
|
npx @frozenproductions/niteo init
|
|
121
|
+
# or, after npm i -g @frozenproductions/niteo
|
|
122
|
+
niteo init
|
|
92
123
|
```
|
|
93
124
|
|
|
94
125
|
The config format is not stable yet, so prefer generating it from the CLI instead of copying old examples.
|
package/package.json
CHANGED
package/src/app.rs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
use anyhow::Result;
|
|
1
|
+
use anyhow::{Context, Result};
|
|
2
2
|
use clap::Parser;
|
|
3
3
|
use std::env;
|
|
4
|
+
use std::fs;
|
|
4
5
|
use std::path::{Path, PathBuf};
|
|
5
6
|
|
|
6
|
-
use crate::cli::{Cli, Command};
|
|
7
|
+
use crate::cli::{Cli, Command, OutputFormat};
|
|
7
8
|
use crate::{config, discovery, git, report, rules};
|
|
8
9
|
|
|
9
10
|
pub fn run() -> Result<()> {
|
|
@@ -18,6 +19,8 @@ pub fn run() -> Result<()> {
|
|
|
18
19
|
cli.options.scope,
|
|
19
20
|
cli.options.verbose,
|
|
20
21
|
cli.options.git,
|
|
22
|
+
cli.options.format,
|
|
23
|
+
cli.options.output,
|
|
21
24
|
),
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -35,6 +38,8 @@ fn lint_workspace(
|
|
|
35
38
|
scope_override: Option<PathBuf>,
|
|
36
39
|
verbose: bool,
|
|
37
40
|
git_flag: bool,
|
|
41
|
+
output_format: OutputFormat,
|
|
42
|
+
output_path: Option<PathBuf>,
|
|
38
43
|
) -> Result<()> {
|
|
39
44
|
let project_config = config::ProjectConfig::resolve(workspace, root_override)?;
|
|
40
45
|
let scan_scope = scope_override.map(|scope| resolve_path(workspace, scope));
|
|
@@ -103,8 +108,34 @@ fn lint_workspace(
|
|
|
103
108
|
all_violations.append(&mut depth_violations);
|
|
104
109
|
|
|
105
110
|
let report = report::Report::new(files, all_violations);
|
|
111
|
+
let rendered_report = match output_format {
|
|
112
|
+
OutputFormat::Text => report.render_text(verbose),
|
|
113
|
+
OutputFormat::Json => report.render_json()?,
|
|
114
|
+
OutputFormat::Sarif => report.render_sarif()?,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
write_report(workspace, output_path, &rendered_report)?;
|
|
118
|
+
|
|
119
|
+
Ok(())
|
|
120
|
+
}
|
|
106
121
|
|
|
107
|
-
|
|
122
|
+
fn write_report(
|
|
123
|
+
workspace: &Path,
|
|
124
|
+
output_path: Option<PathBuf>,
|
|
125
|
+
rendered_report: &str,
|
|
126
|
+
) -> Result<()> {
|
|
127
|
+
let Some(output_path) = output_path else {
|
|
128
|
+
println!("{rendered_report}");
|
|
129
|
+
return Ok(());
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let resolved_output_path = resolve_path(workspace, output_path);
|
|
133
|
+
if let Some(parent) = resolved_output_path.parent() {
|
|
134
|
+
fs::create_dir_all(parent)
|
|
135
|
+
.with_context(|| format!("failed to create {}", parent.display()))?;
|
|
136
|
+
}
|
|
137
|
+
fs::write(&resolved_output_path, rendered_report)
|
|
138
|
+
.with_context(|| format!("failed to write {}", resolved_output_path.display()))?;
|
|
108
139
|
|
|
109
140
|
Ok(())
|
|
110
141
|
}
|
package/src/cli.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use clap::{Args, Parser, Subcommand};
|
|
1
|
+
use clap::{Args, Parser, Subcommand, ValueEnum};
|
|
2
2
|
use std::path::PathBuf;
|
|
3
3
|
|
|
4
4
|
#[derive(Debug, Parser)]
|
|
@@ -32,6 +32,14 @@ pub struct CliOptions {
|
|
|
32
32
|
/// Scan only changed TypeScript files (skips prompt).
|
|
33
33
|
#[arg(long, global = true)]
|
|
34
34
|
pub git: bool,
|
|
35
|
+
|
|
36
|
+
/// Report output format.
|
|
37
|
+
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Text)]
|
|
38
|
+
pub format: OutputFormat,
|
|
39
|
+
|
|
40
|
+
/// Write the report to a file instead of stdout.
|
|
41
|
+
#[arg(short, long, global = true)]
|
|
42
|
+
pub output: Option<PathBuf>,
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
#[derive(Debug, Subcommand)]
|
|
@@ -41,3 +49,10 @@ pub enum Command {
|
|
|
41
49
|
/// Scan the project for structural issues.
|
|
42
50
|
Lint,
|
|
43
51
|
}
|
|
52
|
+
|
|
53
|
+
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
54
|
+
pub enum OutputFormat {
|
|
55
|
+
Text,
|
|
56
|
+
Json,
|
|
57
|
+
Sarif,
|
|
58
|
+
}
|
package/src/report.rs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use serde_json::{Value, json};
|
|
1
3
|
use std::path::PathBuf;
|
|
2
4
|
|
|
3
5
|
use crate::config::Severity;
|
|
@@ -58,6 +60,70 @@ impl Report {
|
|
|
58
60
|
output
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
pub fn render_json(&self) -> Result<String> {
|
|
64
|
+
let report = json!({
|
|
65
|
+
"summary": self.summary_json(),
|
|
66
|
+
"files": self
|
|
67
|
+
.files
|
|
68
|
+
.iter()
|
|
69
|
+
.map(|file| path_to_string(file))
|
|
70
|
+
.collect::<Vec<String>>(),
|
|
71
|
+
"violations": self
|
|
72
|
+
.violations
|
|
73
|
+
.iter()
|
|
74
|
+
.map(violation_json)
|
|
75
|
+
.collect::<Vec<Value>>(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
Ok(serde_json::to_string_pretty(&report)?)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pub fn render_sarif(&self) -> Result<String> {
|
|
82
|
+
let rules = group_by_rule(&self.violations)
|
|
83
|
+
.iter()
|
|
84
|
+
.map(|group| {
|
|
85
|
+
json!({
|
|
86
|
+
"id": group.rule,
|
|
87
|
+
"name": group.rule,
|
|
88
|
+
"shortDescription": {
|
|
89
|
+
"text": group.message,
|
|
90
|
+
},
|
|
91
|
+
"defaultConfiguration": {
|
|
92
|
+
"level": sarif_level(group.severity),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
.collect::<Vec<Value>>();
|
|
97
|
+
|
|
98
|
+
let results = self
|
|
99
|
+
.violations
|
|
100
|
+
.iter()
|
|
101
|
+
.map(sarif_result_json)
|
|
102
|
+
.collect::<Vec<Value>>();
|
|
103
|
+
|
|
104
|
+
let report = json!({
|
|
105
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
106
|
+
"version": "2.1.0",
|
|
107
|
+
"runs": [
|
|
108
|
+
{
|
|
109
|
+
"tool": {
|
|
110
|
+
"driver": {
|
|
111
|
+
"name": "Niteo",
|
|
112
|
+
"informationUri": "https://github.com/FrozenProductions/Niteo",
|
|
113
|
+
"rules": rules,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"results": results,
|
|
117
|
+
"properties": {
|
|
118
|
+
"summary": self.summary_json(),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
Ok(serde_json::to_string_pretty(&report)?)
|
|
125
|
+
}
|
|
126
|
+
|
|
61
127
|
fn count_by_severity(&self, severity: Severity) -> usize {
|
|
62
128
|
self.violations
|
|
63
129
|
.iter()
|
|
@@ -76,6 +142,90 @@ impl Report {
|
|
|
76
142
|
|
|
77
143
|
100usize.saturating_sub(penalty)
|
|
78
144
|
}
|
|
145
|
+
|
|
146
|
+
fn summary_json(&self) -> Value {
|
|
147
|
+
let warning_count = self.count_by_severity(Severity::Warn);
|
|
148
|
+
let error_count = self.count_by_severity(Severity::Error);
|
|
149
|
+
let info_count = self.count_by_severity(Severity::Info);
|
|
150
|
+
|
|
151
|
+
json!({
|
|
152
|
+
"filesScanned": self.files.len(),
|
|
153
|
+
"violations": self.violations.len(),
|
|
154
|
+
"errors": error_count,
|
|
155
|
+
"warnings": warning_count,
|
|
156
|
+
"info": info_count,
|
|
157
|
+
"score": self.score(error_count, warning_count),
|
|
158
|
+
"status": status_label(error_count, warning_count, info_count),
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn violation_json(violation: &Violation) -> Value {
|
|
164
|
+
json!({
|
|
165
|
+
"file": path_to_string(&violation.file),
|
|
166
|
+
"line": violation.line,
|
|
167
|
+
"column": violation.column,
|
|
168
|
+
"rule": violation.rule,
|
|
169
|
+
"message": violation.message,
|
|
170
|
+
"severity": severity_label(violation.severity),
|
|
171
|
+
"detail": violation.detail,
|
|
172
|
+
"subject": violation.subject,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn sarif_result_json(violation: &Violation) -> Value {
|
|
177
|
+
let mut message = violation.message.to_string();
|
|
178
|
+
if let Some(detail) = &violation.detail {
|
|
179
|
+
message.push(' ');
|
|
180
|
+
message.push_str(detail);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
json!({
|
|
184
|
+
"ruleId": violation.rule,
|
|
185
|
+
"level": sarif_level(violation.severity),
|
|
186
|
+
"message": {
|
|
187
|
+
"text": message,
|
|
188
|
+
},
|
|
189
|
+
"locations": [
|
|
190
|
+
{
|
|
191
|
+
"physicalLocation": {
|
|
192
|
+
"artifactLocation": {
|
|
193
|
+
"uri": path_to_string(&violation.file),
|
|
194
|
+
},
|
|
195
|
+
"region": {
|
|
196
|
+
"startLine": violation.line.unwrap_or(1),
|
|
197
|
+
"startColumn": violation.column.unwrap_or(1),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
"properties": {
|
|
203
|
+
"severity": severity_label(violation.severity),
|
|
204
|
+
"subject": violation.subject,
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn path_to_string(path: &PathBuf) -> String {
|
|
210
|
+
path.display().to_string()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn severity_label(severity: Severity) -> &'static str {
|
|
214
|
+
match severity {
|
|
215
|
+
Severity::Error => "error",
|
|
216
|
+
Severity::Warn => "warning",
|
|
217
|
+
Severity::Info => "info",
|
|
218
|
+
Severity::Off => "off",
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn sarif_level(severity: Severity) -> &'static str {
|
|
223
|
+
match severity {
|
|
224
|
+
Severity::Error => "error",
|
|
225
|
+
Severity::Warn => "warning",
|
|
226
|
+
Severity::Info => "note",
|
|
227
|
+
Severity::Off => "none",
|
|
228
|
+
}
|
|
79
229
|
}
|
|
80
230
|
|
|
81
231
|
#[derive(Debug)]
|