1extern crate failure;
2
3#[macro_use]
4extern crate serde_derive;
5
6extern crate serde;
7extern crate serde_json;
8
9extern crate wasm_bindgen;
10
11use std::cmp::Ordering;
12use std::collections::HashMap;
13use std::fs::File;
14use std::io;
15use std::result::Result;
16
17use failure::Error;
18use failure::ResultExt;
19use wasm_bindgen::prelude::*;
20
21pub const STDIN_STR: &str = "-";
22const NO_ADVISORIES_FOUND: &str = "No advisories found after filtering.";
23
24pub type AdvisoryID = u32;
25pub type AdvisoryURL = String;
26
27#[derive(Serialize, Deserialize, Debug)]
28pub struct NPMAudit {
29 pub advisories: HashMap<AdvisoryID, Advisory>,
30}
31
32#[derive(Serialize, Deserialize, Debug, Eq)]
33pub struct Advisory {
34 pub findings: Vec<AdvisoryFinding>,
35 pub id: AdvisoryID,
36 pub title: String,
37 pub module_name: String,
38 pub overview: String,
39 pub recommendation: String,
40 pub severity: String,
41 pub url: AdvisoryURL,
42}
43
44impl Ord for Advisory {
45 fn cmp(&self, other: &Advisory) -> Ordering {
46 self.url.cmp(&other.url)
47 }
48}
49
50impl PartialOrd for Advisory {
51 fn partial_cmp(&self, other: &Advisory) -> Option<Ordering> {
52 Some(self.cmp(other))
53 }
54}
55
56impl PartialEq for Advisory {
57 fn eq(&self, other: &Advisory) -> bool {
58 self.url == other.url
59 }
60}
61
62#[derive(Serialize, Deserialize, Debug, Eq)]
63pub struct AdvisoryFinding {
64 pub version: String,
65 pub paths: Vec<String>,
66
67 pub dev: Option<bool>,
69 pub optional: Option<bool>,
70 pub bundled: Option<bool>,
71}
72
73impl PartialEq for AdvisoryFinding {
74 fn eq(&self, other: &AdvisoryFinding) -> bool {
75 self.version == other.version && self.paths == other.paths
76 }
77}
78
79#[derive(Serialize, Deserialize, Debug)]
80pub struct NSPConfig {
81 pub exceptions: Vec<AdvisoryURL>,
82}
83
84pub fn parse_audit(path: &str) -> Result<NPMAudit, Error> {
85 let audit: NPMAudit;
86
87 if path == STDIN_STR {
88 audit = serde_json::from_reader(io::stdin())
89 .with_context(|e| format!("Error parsing audit JSON from stdin: {}", e))?
90 } else {
91 let fin = File::open(path)
92 .with_context(|e| format!("Error opening audit JSON {}: {}", path, e))?;
93 audit = serde_json::from_reader(fin)
94 .with_context(|e| format!("Error parsing audit JSON: {}", e))?
95 }
96 Ok(audit)
97}
98
99fn parse_audit_from_str(s: &str) -> Result<NPMAudit, Error> {
100 let audit: NPMAudit =
101 serde_json::from_str(s).with_context(|e| format!("Error parsing audit JSON: {}", e))?;
102
103 Ok(audit)
104}
105
106fn parse_nsp_config_from_str(s: &str) -> Result<NSPConfig, Error> {
107 let config: NSPConfig = serde_json::from_str(s)
108 .with_context(|e| format!("Error parsing nsp config JSON: {}", e))?;
109
110 Ok(config)
111}
112
113pub fn parse_nsp_config(path: &str) -> Result<NSPConfig, Error> {
114 let config: NSPConfig;
115
116 if path == STDIN_STR {
117 config = serde_json::from_reader(io::stdin())
118 .with_context(|e| format!("Error parsing nsp config JSON from stdin: {}", e))?
119 } else {
120 let fin = File::open(path)
121 .with_context(|e| format!("Error opening nsp config JSON {}: {}", path, e))?;
122 config = serde_json::from_reader(fin)
123 .with_context(|e| format!("Error parsing nsp config JSON: {}", e))?
124 }
125 Ok(config)
126}
127
128pub fn filter_advisories_by_url(
129 audit: NPMAudit,
130 nsp_config: &NSPConfig,
131) -> Result<Vec<Advisory>, Error> {
132 let mut unacked_advisories: Vec<Advisory> = vec![];
133
134 for (_, advisory) in audit.advisories {
135 if !nsp_config.exceptions.contains(&advisory.url) {
136 unacked_advisories.push(advisory)
137 }
138 }
139
140 unacked_advisories.sort_unstable_by_key(|a| a.id);
141 Ok(unacked_advisories)
142}
143
144#[wasm_bindgen]
145pub fn version() -> String {
146 let (maj, min, pat) = (
147 option_env!("CARGO_PKG_VERSION_MAJOR"),
148 option_env!("CARGO_PKG_VERSION_MINOR"),
149 option_env!("CARGO_PKG_VERSION_PATCH"),
150 );
151 match (maj, min, pat) {
152 (Some(maj), Some(min), Some(pat)) => format!("{}.{}.{}", maj, min, pat),
153 _ => "".to_owned(),
154 }
155}
156
157pub fn parse_files_and_filter_advisories_by_url(
158 audit_path: &str,
159 nsp_config_path: &str,
160) -> Result<Vec<Advisory>, Error> {
161 let nsp_config = parse_nsp_config(nsp_config_path)?;
162 let audit = parse_audit(audit_path)?;
163 let unacked_advisories = filter_advisories_by_url(audit, &nsp_config)?;
164 Ok(unacked_advisories)
165}
166
167pub fn parse_strs_and_filter_advisories_by_url(
168 audit_str: &str,
169 nsp_config_str: &str,
170) -> Result<Vec<Advisory>, Error> {
171 let nsp_config = parse_nsp_config_from_str(nsp_config_str)?;
172 let audit = parse_audit_from_str(audit_str)?;
173 let unacked_advisories = filter_advisories_by_url(audit, &nsp_config)?;
174 Ok(unacked_advisories)
175}
176
177pub fn get_advisory_urls(advisories: Vec<Advisory>) -> Vec<AdvisoryURL> {
178 advisories
179 .into_iter()
180 .map(|a| a.url)
181 .collect::<Vec<AdvisoryURL>>()
182}
183
184pub fn format_json_output(advisories: &[Advisory]) -> Result<String, Error> {
185 let formatted = serde_json::to_string_pretty(&advisories).with_context(|e| {
186 format!(
187 "{{\"error\": \"error formatting advisories as json: {}\"}}",
188 e
189 )
190 })?;
191 Ok(formatted)
192}
193
194#[wasm_bindgen]
195extern "C" {
196 #[wasm_bindgen(js_namespace = console)]
197 fn log(msg: &str);
198
199 #[wasm_bindgen(js_namespace = console)]
200 fn error(msg: &str);
201}
202
203macro_rules! log {
205 ($($t:tt)*) => (log(&format!($($t)*)))
206}
207macro_rules! err {
208 ($($t:tt)*) => (error(&format!($($t)*)))
209}
210
211#[wasm_bindgen]
212pub fn run_wasm(audit_str: &str, nsp_config_str: &str, output_json: bool) -> i32 {
213 match parse_strs_and_filter_advisories_by_url(audit_str, nsp_config_str) {
214 Ok(unacked_advisories) => {
215 if output_json {
216 log!("{}", format_json_output(&unacked_advisories).unwrap())
217 }
218 if unacked_advisories.is_empty() {
219 if !output_json {
220 log!("{}", NO_ADVISORIES_FOUND);
221 }
222 return 0;
223 } else if !unacked_advisories.is_empty() {
224 if !output_json {
225 err!(
226 "Unfiltered advisories:\n {}",
227 get_advisory_urls(unacked_advisories).join("\n ")
228 );
229 }
230 return 1;
231 }
232 unimplemented!() }
234 Err(err) => {
235 err!("{}", err);
236 2
237 }
238 }
239}
240
241pub fn run(audit_path: &str, nsp_config_path: &str, output_json: bool) -> i32 {
242 match parse_files_and_filter_advisories_by_url(audit_path, nsp_config_path) {
243 Ok(unacked_advisories) => {
244 if output_json {
245 println!("{}", format_json_output(&unacked_advisories).unwrap())
246 }
247 if unacked_advisories.is_empty() {
248 if !output_json {
249 println!("{}", NO_ADVISORIES_FOUND);
250 }
251 return 0;
252 } else if !unacked_advisories.is_empty() {
253 if !output_json {
254 eprintln!(
255 "Unfiltered advisories:\n {}",
256 get_advisory_urls(unacked_advisories).join("\n ")
257 );
258 }
259 return 1;
260 }
261 unimplemented!() }
263 Err(err) => {
264 eprintln!("{}", err);
265 2
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn setup_test_adv_51600() -> Advisory {
276 Advisory {
277 findings: vec![AdvisoryFinding {
278 version: "2.16.3".to_string(),
279 paths: vec![
280 "david>npm>npm-registry-client>request>hawk>boom>hoek".to_string(),
281 "david>npm>npm-registry-client>request>hawk>hoek".to_string(),
282 ],
283 dev: Some(false),
284 optional: Some(false),
285 bundled: Some(false),
286 }],
287 id: 51600,
288 title: "Prototype Pollution".to_string(),
289 module_name: "hoek".to_string(),
290 url: "https://nodesecurity.io/advisories/51600".to_string(),
291 overview: "Versions of `hoek` prior to 4.2.1 and 5.0.3 are vulnerable to prototype pollution.\n\nThe `merge` function, and the `applyToDefaults` and
292 `applyToDefaultsWithShallow` functions which leverage `merge` behind the scenes, are vulnerable to a prototype pollution attack when provided an _unvalidated
293_ payload created from a JSON string containing the `__proto__` property.\n\nThis can be demonstrated like so:\n\n```javascript\nvar Hoek = require('hoek');\n
294var malicious_payload = '{\"__proto__\":{\"oops\":\"It works !\"}}';\n\nvar a = {};\nconsole.log(\"Before : \" + a.oops);\nHoek.merge({}, JSON.parse(malicious
295_payload));\nconsole.log(\"After : \" + a.oops);\n```\n\nThis type of attack can be used to overwrite existing properties causing a potential denial of servic
296e.".to_string(),
297 severity: "moderate".to_string(),
298 recommendation: "Update to version 4.2.1, 5.0.3 or later.".to_string(),
299 }
300 }
301
302 fn setup_test_adv_566() -> Advisory {
303 Advisory {
304 findings: vec![AdvisoryFinding {
305 version: "2.16.3".to_string(),
306 paths: vec![
307 "david>npm>npm-registry-client>request>hawk>boom>hoek".to_string(),
308 "david>npm>npm-registry-client>request>hawk>hoek".to_string(),
309 ],
310 dev: Some(false),
311 optional: Some(false),
312 bundled: Some(false),
313 }],
314 id: 566,
315 title: "Prototype Pollution".to_string(),
316 module_name: "hoek".to_string(),
317 url: "https://nodesecurity.io/advisories/566".to_string(),
318 overview: "Versions of `hoek` prior to 4.2.1 and 5.0.3 are vulnerable to prototype pollution.\n\nThe `merge` function, and the `applyToDefaults` and
319 `applyToDefaultsWithShallow` functions which leverage `merge` behind the scenes, are vulnerable to a prototype pollution attack when provided an _unvalidated
320_ payload created from a JSON string containing the `__proto__` property.\n\nThis can be demonstrated like so:\n\n```javascript\nvar Hoek = require('hoek');\n
321var malicious_payload = '{\"__proto__\":{\"oops\":\"It works !\"}}';\n\nvar a = {};\nconsole.log(\"Before : \" + a.oops);\nHoek.merge({}, JSON.parse(malicious
322_payload));\nconsole.log(\"After : \" + a.oops);\n```\n\nThis type of attack can be used to overwrite existing properties causing a potential denial of servic
323e.".to_string(),
324 severity: "moderate".to_string(),
325 recommendation: "Update to version 4.2.1, 5.0.3 or later.".to_string(),
326 }
327 }
328
329 fn setup_test_adv_577() -> Advisory {
330 Advisory {
331 findings: vec![AdvisoryFinding {
332 version: "4.12.0".to_string(),
333 paths: vec!["jpm>firefox-profile>lodash".to_string()],
334 dev: Some(false),
335 optional: Some(false),
336 bundled: Some(false),
337 }],
338 id: 577,
339 title: "Prototype Pollution".to_string(),
340 module_name: "lodash".to_string(),
341 url: "https://nodesecurity.io/advisories/577".to_string(),
342 overview: "Versions of `lodash` before 4.17.5 are vulnerable to prototype pollution. \n\nThe vulnerable functions are 'defaultsDeep', 'merge', and 'me
343rgeWith' which allow a malicious user to modify the prototype of `Object` via `__proto__` causing the addition or modification of an existing property that wi
344ll exist on all objects.\n\n".to_string(),
345 severity: "low".to_string(),
346 recommendation: "Update to version 4.17.5 or later.".to_string(),
347 }
348 }
349
350 fn setup_test_audit() -> NPMAudit {
351 let mut advisories = HashMap::new();
352 advisories.insert(566, setup_test_adv_566());
353 advisories.insert(577, setup_test_adv_577());
354
355 NPMAudit { advisories }
356 }
357
358 #[test]
359 fn it_should_treat_advisories_with_the_same_url_as_equal() {
360 assert_eq!(setup_test_adv_566(), setup_test_adv_566())
361 }
362
363 #[test]
364 fn it_should_treat_advisories_with_different_urls_as_not_equal() {
365 assert_ne!(setup_test_adv_566(), setup_test_adv_577())
366 }
367
368 #[test]
369 fn it_should_filter_none_for_empty_nsp_config() {
370 let audit = setup_test_audit();
371 let empty_nsp_config = &NSPConfig { exceptions: vec![] };
372
373 let empty_filtered_result = filter_advisories_by_url(audit, empty_nsp_config);
374 assert!(empty_filtered_result.is_ok());
375 let empty_filtered = get_advisory_urls(empty_filtered_result.unwrap());
376 assert_eq!(
377 vec![
378 "https://nodesecurity.io/advisories/566".to_string(),
379 "https://nodesecurity.io/advisories/577".to_string(),
380 ],
381 empty_filtered
382 );
383 }
384
385 #[test]
386 fn it_should_filter_an_advisory() {
387 let audit = setup_test_audit();
388 let nsp_config = &NSPConfig {
389 exceptions: vec![
390 "https://nodesecurity.io/advisories/577".to_string(),
391 "https://nodesecurity.io/advisories/566".to_string(),
392 ],
393 };
394
395 let filtered_result = filter_advisories_by_url(audit, nsp_config);
396 assert!(filtered_result.is_ok());
397 let filtered = filtered_result.unwrap();
398 assert!(filtered.is_empty());
399 }
400
401 #[test]
402 fn it_should_filter_an_advisory_into_an_numerically_sorted_list() {
403 let mut audit = setup_test_audit();
404 audit.advisories.insert(5660, setup_test_adv_51600());
405
406 let nsp_config = &NSPConfig { exceptions: vec![] };
407
408 let filtered_result = filter_advisories_by_url(audit, nsp_config);
409 assert!(filtered_result.is_ok());
410 let filtered = get_advisory_urls(filtered_result.unwrap());
411 assert_eq!(
412 vec![
413 "https://nodesecurity.io/advisories/566".to_string(),
414 "https://nodesecurity.io/advisories/577".to_string(),
415 "https://nodesecurity.io/advisories/51600".to_string(),
416 ],
417 filtered
418 );
419 }
420}