1use crate::models::{
23 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha1Digest, Sha512Digest,
24};
25use crate::parser_warn as warn;
26use crate::parsers::utils::{
27 MAX_ITERATION_COUNT, MAX_RECURSION_DEPTH, RecursionGuard, npm_purl, parse_sri,
28 read_file_to_string, truncate_field,
29};
30use serde_json::Value;
31use std::collections::HashMap;
32use std::path::Path;
33
34use super::PackageParser;
35
36const FIELD_LOCKFILE_VERSION: &str = "lockfileVersion";
38const FIELD_NAME: &str = "name";
39const FIELD_VERSION: &str = "version";
40const FIELD_DEPENDENCIES: &str = "dependencies";
41const FIELD_PACKAGES: &str = "packages";
42const FIELD_RESOLVED: &str = "resolved";
43const FIELD_INTEGRITY: &str = "integrity";
44const FIELD_DEV: &str = "dev";
45const FIELD_OPTIONAL: &str = "optional";
46const FIELD_DEV_OPTIONAL: &str = "devOptional";
47const FIELD_LINK: &str = "link";
48
49pub struct NpmLockParser;
54
55impl PackageParser for NpmLockParser {
56 const PACKAGE_TYPE: PackageType = PackageType::Npm;
57
58 fn is_match(path: &Path) -> bool {
59 path.file_name()
60 .and_then(|name| name.to_str())
61 .map(|name| {
62 name == "package-lock.json"
63 || name == ".package-lock.json"
64 || name == "npm-shrinkwrap.json"
65 || name == ".npm-shrinkwrap.json"
66 })
67 .unwrap_or(false)
68 }
69
70 fn extract_packages(path: &Path) -> Vec<PackageData> {
71 let content = match read_file_to_string(path, None) {
72 Ok(content) => content,
73 Err(e) => {
74 warn!("Failed to read package-lock.json at {:?}: {}", path, e);
75 return vec![default_package_data()];
76 }
77 };
78
79 let json: Value = match serde_json::from_str(&content) {
80 Ok(json) => json,
81 Err(e) => {
82 warn!("Failed to parse package-lock.json at {:?}: {}", path, e);
83 return vec![default_package_data()];
84 }
85 };
86
87 let lockfile_version = json
88 .get(FIELD_LOCKFILE_VERSION)
89 .and_then(|v| v.as_i64())
90 .unwrap_or(1);
91
92 let root_name = truncate_field(
93 json.get(FIELD_NAME)
94 .and_then(|v| v.as_str())
95 .unwrap_or("")
96 .to_string(),
97 );
98
99 let root_version = truncate_field(
100 json.get(FIELD_VERSION)
101 .and_then(|v| v.as_str())
102 .unwrap_or("")
103 .to_string(),
104 );
105
106 vec![if lockfile_version == 1 {
107 parse_lockfile_v1(&json, root_name, root_version, lockfile_version)
108 } else {
109 parse_lockfile_v2_plus(&json, root_name, root_version, lockfile_version)
110 }]
111 }
112}
113
114fn default_package_data() -> PackageData {
116 PackageData {
117 package_type: Some(NpmLockParser::PACKAGE_TYPE),
118 datasource_id: Some(DatasourceId::NpmPackageLockJson),
119 ..Default::default()
120 }
121}
122
123fn parse_lockfile_v2_plus(
125 json: &Value,
126 root_name: String,
127 root_version: String,
128 lockfile_version: i64,
129) -> PackageData {
130 let packages = match json.get(FIELD_PACKAGES).and_then(|v| v.as_object()) {
131 Some(packages) => packages,
132 None => {
133 warn!("No 'packages' field found in lockfile v2+");
134 return default_package_data();
135 }
136 };
137
138 let (root_name, root_version) = extract_root_package_identity(json, root_name, root_version);
139 let (namespace, name, version, purl) =
140 normalize_root_package_metadata(&root_name, &root_version);
141
142 let mut root_deps = std::collections::HashSet::new();
144
145 if let Some(root_deps_obj) = json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
147 for key in root_deps_obj.keys().take(MAX_ITERATION_COUNT) {
148 root_deps.insert(key.clone());
149 }
150 }
151 if let Some(root_dev_deps_obj) = json.get("devDependencies").and_then(|v| v.as_object()) {
152 for key in root_dev_deps_obj.keys().take(MAX_ITERATION_COUNT) {
153 root_deps.insert(key.clone());
154 }
155 }
156 if let Some(root_package) = packages.get("").and_then(|value| value.as_object()) {
157 collect_root_dependency_names(root_package.get(FIELD_DEPENDENCIES), &mut root_deps);
158 collect_root_dependency_names(root_package.get("devDependencies"), &mut root_deps);
159 collect_root_dependency_names(root_package.get("optionalDependencies"), &mut root_deps);
160 }
161
162 let mut dependencies = Vec::new();
163
164 for (key, value) in packages.iter().take(MAX_ITERATION_COUNT) {
165 if key.is_empty() {
167 continue;
168 }
169
170 let install_name = extract_package_name_from_path(key);
172 if install_name.is_empty() {
173 continue;
174 }
175
176 let package_name = value
177 .get(FIELD_NAME)
178 .and_then(|v| v.as_str())
179 .map(str::trim)
180 .filter(|name| !name.is_empty())
181 .map(str::to_string)
182 .unwrap_or_else(|| install_name.clone());
183
184 let version = value
185 .get(FIELD_VERSION)
186 .and_then(|v| v.as_str())
187 .map(|v| truncate_field(v.to_string()));
188
189 let is_dev = value
190 .get(FIELD_DEV)
191 .and_then(|v| v.as_bool())
192 .unwrap_or(false);
193 let is_optional = value
194 .get(FIELD_OPTIONAL)
195 .and_then(|v| v.as_bool())
196 .unwrap_or(false);
197 let is_dev_optional = value
198 .get(FIELD_DEV_OPTIONAL)
199 .and_then(|v| v.as_bool())
200 .unwrap_or(false);
201
202 let resolved = value
203 .get(FIELD_RESOLVED)
204 .and_then(|v| v.as_str())
205 .map(|v| truncate_field(v.to_string()));
206 let integrity = value.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
207 let from = value.get("from").and_then(|v| v.as_str());
208 let in_bundle = value
209 .get("inBundle")
210 .and_then(|v| v.as_bool())
211 .unwrap_or(false);
212 let is_link = value
213 .get(FIELD_LINK)
214 .and_then(|v| v.as_bool())
215 .unwrap_or(false);
216 let is_direct = root_deps.contains(&install_name) && is_direct_dependency_path(key);
217
218 let dependency = match version {
219 Some(version) => build_npm_dependency(
220 &package_name,
221 version,
222 is_dev,
223 is_dev_optional,
224 is_optional,
225 resolved,
226 integrity,
227 is_direct,
228 from,
229 in_bundle,
230 Vec::new(),
231 ),
232 None if is_link => build_link_dependency(
233 &package_name,
234 is_dev,
235 is_dev_optional,
236 is_optional,
237 resolved,
238 is_direct,
239 ),
240 None => continue,
241 };
242
243 dependencies.push(dependency);
244 }
245
246 let extra_data = Some(HashMap::from([(
247 "lockfileVersion".to_string(),
248 Value::from(lockfile_version),
249 )]));
250
251 PackageData {
252 package_type: Some(NpmLockParser::PACKAGE_TYPE),
253 namespace: namespace.clone(),
254 name,
255 version,
256 qualifiers: None,
257 subpath: None,
258 primary_language: None,
259 description: None,
260 release_date: None,
261 parties: Vec::new(),
262 keywords: Vec::new(),
263 homepage_url: None,
264 download_url: None,
265 size: None,
266 sha1: None,
267 md5: None,
268 sha256: None,
269 sha512: None,
270 bug_tracking_url: None,
271 code_view_url: None,
272 vcs_url: None,
273 copyright: None,
274 holder: None,
275 declared_license_expression: None,
276 declared_license_expression_spdx: None,
277 license_detections: Vec::new(),
278 other_license_expression: None,
279 other_license_expression_spdx: None,
280 other_license_detections: Vec::new(),
281 extracted_license_statement: None,
282 notice_text: None,
283 source_packages: Vec::new(),
284 file_references: Vec::new(),
285 is_private: false,
286 is_virtual: false,
287 extra_data,
288 dependencies,
289 repository_homepage_url: None,
290 repository_download_url: None,
291 api_data_url: None,
292 datasource_id: Some(DatasourceId::NpmPackageLockJson),
293 purl,
294 }
295}
296
297fn parse_lockfile_v1(
299 json: &Value,
300 root_name: String,
301 root_version: String,
302 _lockfile_version: i64,
303) -> PackageData {
304 let dependencies_obj = match json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
305 Some(deps) => deps,
306 None => {
307 warn!("No 'dependencies' field found in lockfile v1");
308 return default_package_data();
309 }
310 };
311
312 let (namespace, name, version, purl) =
313 normalize_root_package_metadata(&root_name, &root_version);
314
315 let dependencies = parse_dependencies_v1(dependencies_obj);
316
317 PackageData {
318 package_type: Some(NpmLockParser::PACKAGE_TYPE),
319 namespace: namespace.clone(),
320 name,
321 version,
322 qualifiers: None,
323 subpath: None,
324 primary_language: None,
325 description: None,
326 release_date: None,
327 parties: Vec::new(),
328 keywords: Vec::new(),
329 homepage_url: None,
330 download_url: None,
331 size: None,
332 sha1: None,
333 md5: None,
334 sha256: None,
335 sha512: None,
336 bug_tracking_url: None,
337 code_view_url: None,
338 vcs_url: None,
339 copyright: None,
340 holder: None,
341 declared_license_expression: None,
342 declared_license_expression_spdx: None,
343 license_detections: Vec::new(),
344 other_license_expression: None,
345 other_license_expression_spdx: None,
346 other_license_detections: Vec::new(),
347 extracted_license_statement: None,
348 notice_text: None,
349 source_packages: Vec::new(),
350 file_references: Vec::new(),
351 is_private: false,
352 is_virtual: false,
353 extra_data: None,
354 dependencies,
355 repository_homepage_url: None,
356 repository_download_url: None,
357 api_data_url: None,
358 datasource_id: Some(DatasourceId::NpmPackageLockJson),
359 purl,
360 }
361}
362
363fn parse_dependencies_v1(dependencies_obj: &serde_json::Map<String, Value>) -> Vec<Dependency> {
368 let mut guard = RecursionGuard::<()>::depth_only();
369 parse_dependencies_v1_with_depth(dependencies_obj, &mut guard)
370}
371
372fn parse_dependencies_v1_with_depth(
374 dependencies_obj: &serde_json::Map<String, Value>,
375 guard: &mut RecursionGuard<()>,
376) -> Vec<Dependency> {
377 if guard.descend() {
378 warn!(
379 "Max recursion depth {} exceeded in v1 dependency parsing",
380 MAX_RECURSION_DEPTH
381 );
382 return Vec::new();
383 }
384
385 let mut dependencies = Vec::new();
386
387 for (package_name, dep_data) in dependencies_obj.iter().take(MAX_ITERATION_COUNT) {
388 let version = match dep_data.get(FIELD_VERSION).and_then(|v| v.as_str()) {
389 Some(v) => truncate_field(v.to_string()),
390 None => continue,
391 };
392
393 let is_dev = dep_data
394 .get(FIELD_DEV)
395 .and_then(|v| v.as_bool())
396 .unwrap_or(false);
397 let is_optional = dep_data
398 .get(FIELD_OPTIONAL)
399 .and_then(|v| v.as_bool())
400 .unwrap_or(false);
401
402 let resolved = dep_data
403 .get(FIELD_RESOLVED)
404 .and_then(|v| v.as_str())
405 .map(|v| truncate_field(v.to_string()));
406 let integrity = dep_data.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
407 let from = dep_data.get("from").and_then(|v| v.as_str());
408 let in_bundle = dep_data
409 .get("inBundle")
410 .and_then(|v| v.as_bool())
411 .unwrap_or(false);
412
413 let nested_deps = dep_data
414 .get(FIELD_DEPENDENCIES)
415 .and_then(|v| v.as_object())
416 .map(|nested| parse_dependencies_v1_with_depth(nested, guard))
417 .unwrap_or_default();
418
419 let is_direct = guard.depth() == 1;
420
421 let dependency = build_npm_dependency(
422 package_name,
423 version,
424 is_dev,
425 false,
426 is_optional,
427 resolved,
428 integrity,
429 is_direct,
430 from,
431 in_bundle,
432 nested_deps,
433 );
434
435 dependencies.push(dependency);
436 }
437
438 guard.ascend();
439 dependencies
440}
441
442fn extract_namespace_and_name(package_name: &str) -> (String, String) {
445 if package_name.starts_with('@') {
446 let parts: Vec<&str> = package_name.splitn(2, '/').collect();
448 if parts.len() == 2 {
449 (parts[0].to_string(), parts[1].to_string())
450 } else {
451 (String::new(), package_name.to_string())
453 }
454 } else {
455 (String::new(), package_name.to_string())
457 }
458}
459
460fn extract_package_name_from_path(path: &str) -> String {
462 if let Some(pos) = path.rfind("node_modules/") {
464 let after_node_modules = &path[pos + "node_modules/".len()..];
465
466 if after_node_modules.starts_with('@') {
468 if let Some(slash_pos) = after_node_modules.find('/') {
470 let scope_and_package = &after_node_modules[..=slash_pos];
471 let remaining = &after_node_modules[slash_pos + 1..];
473 if let Some(next_slash) = remaining.find('/') {
474 return format!("{}{}", scope_and_package, &remaining[..next_slash]);
476 } else {
477 return after_node_modules.to_string();
479 }
480 }
481 } else {
482 if let Some(slash_pos) = after_node_modules.find('/') {
484 return after_node_modules[..slash_pos].to_string();
485 } else {
486 return after_node_modules.to_string();
487 }
488 }
489 }
490
491 path.to_string()
492}
493
494fn create_purl(namespace: &str, name: &str, version: Option<&str>) -> Option<String> {
495 let full_name = if namespace.is_empty() {
496 name.to_string()
497 } else {
498 format!("{}/{}", namespace, name)
499 };
500 npm_purl(&full_name, version.filter(|value| !value.is_empty()))
501}
502
503fn normalize_root_package_metadata(
504 root_name: &str,
505 root_version: &str,
506) -> (
507 Option<String>,
508 Option<String>,
509 Option<String>,
510 Option<String>,
511) {
512 let (namespace, name) = extract_namespace_and_name(root_name);
513 let normalized_name = non_empty_string(&name);
514 let normalized_namespace = normalized_name.as_ref().map(|_| namespace);
515 let normalized_version = normalized_name
516 .as_ref()
517 .and_then(|_| non_empty_string(root_version));
518 let purl = normalized_name.as_deref().and_then(|name| {
519 create_purl(
520 normalized_namespace.as_deref().unwrap_or(""),
521 name,
522 normalized_version.as_deref(),
523 )
524 });
525
526 (
527 normalized_namespace,
528 normalized_name,
529 normalized_version,
530 purl,
531 )
532}
533
534fn extract_root_package_identity(
535 json: &Value,
536 root_name: String,
537 root_version: String,
538) -> (String, String) {
539 let root_package = json
540 .get(FIELD_PACKAGES)
541 .and_then(|value| value.as_object())
542 .and_then(|packages| packages.get(""))
543 .and_then(|value| value.as_object());
544
545 let name = non_empty_string(&root_name).or_else(|| {
546 root_package
547 .and_then(|package| package.get(FIELD_NAME))
548 .and_then(|value| value.as_str())
549 .map(str::to_string)
550 .filter(|value| !value.trim().is_empty())
551 });
552 let version = non_empty_string(&root_version).or_else(|| {
553 root_package
554 .and_then(|package| package.get(FIELD_VERSION))
555 .and_then(|value| value.as_str())
556 .map(str::to_string)
557 .filter(|value| !value.trim().is_empty())
558 });
559
560 (name.unwrap_or_default(), version.unwrap_or_default())
561}
562
563fn non_empty_string(value: &str) -> Option<String> {
564 let trimmed = value.trim();
565 if trimmed.is_empty() {
566 None
567 } else {
568 Some(trimmed.to_string())
569 }
570}
571
572fn collect_root_dependency_names(
573 value: Option<&Value>,
574 root_deps: &mut std::collections::HashSet<String>,
575) {
576 if let Some(entries) = value.and_then(|value| value.as_object()) {
577 for key in entries.keys().take(MAX_ITERATION_COUNT) {
578 root_deps.insert(key.clone());
579 }
580 }
581}
582
583fn is_direct_dependency_path(package_path: &str) -> bool {
584 let node_modules_count = package_path.matches("node_modules/").count();
585
586 match node_modules_count {
587 0 => true,
588 1 => package_path.starts_with("node_modules/") || package_path.starts_with(".pnpm/"),
589 _ => false,
590 }
591}
592
593fn parse_integrity_field(integrity: Option<&str>) -> (Option<String>, Option<String>) {
596 let integrity = match integrity {
597 Some(i) => i,
598 None => return (None, None),
599 };
600
601 match parse_sri(integrity) {
602 Some((algo, hex_digest)) => match algo.as_str() {
603 "sha1" => (Some(hex_digest), None),
604 "sha512" => (None, Some(hex_digest)),
605 _ => (None, None),
606 },
607 None => (None, None),
608 }
609}
610
611fn parse_resolved_url(url: &str) -> Option<String> {
614 if let Some(hash_pos) = url.rfind('#') {
616 let hash = &url[hash_pos + 1..];
617 if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
619 return Some(hash.to_string());
620 }
621 }
622 None
623}
624
625fn determine_scope(
628 is_dev: bool,
629 is_dev_optional: bool,
630 is_optional: bool,
631) -> (&'static str, bool, bool) {
632 if is_dev || is_dev_optional {
633 ("devDependencies", false, true)
634 } else if is_optional {
635 ("dependencies", true, true)
636 } else {
637 ("dependencies", true, false)
638 }
639}
640
641fn parse_npm_alias_spec(version_spec: &str) -> Option<(String, String, String)> {
642 let aliased_spec = version_spec.strip_prefix("npm:")?;
643 let (aliased_name, constraint) = aliased_spec.rsplit_once('@')?;
644 let (namespace, name) = extract_namespace_and_name(aliased_name);
645
646 if name.is_empty() || constraint.trim().is_empty() {
647 None
648 } else {
649 Some((namespace, name, constraint.to_string()))
650 }
651}
652
653fn is_exact_version(version: &str) -> bool {
654 let version = version.trim();
655
656 if version.is_empty() {
657 return false;
658 }
659
660 if version.starts_with('~')
661 || version.starts_with('^')
662 || version.starts_with('>')
663 || version.starts_with('<')
664 || version.starts_with('=')
665 || version.starts_with('*')
666 || version.contains("||")
667 || version.contains(" - ")
668 {
669 return false;
670 }
671
672 !is_non_version_dependency(version)
673}
674
675fn is_non_version_dependency(version: &str) -> bool {
676 let version = version.trim();
677
678 version.starts_with("http://")
679 || version.starts_with("https://")
680 || version.starts_with("git://")
681 || version.starts_with("git+ssh://")
682 || version.starts_with("git+http://")
683 || version.starts_with("git+https://")
684 || version.starts_with("git+file://")
685 || version.starts_with("git@")
686 || version.starts_with("file:")
687 || version.starts_with("link:")
688 || version.starts_with("github:")
689 || version.starts_with("gitlab:")
690 || version.starts_with("bitbucket:")
691 || version.starts_with("gist:")
692}
693
694fn non_version_download_url(version: &str, resolved: Option<&str>) -> Option<String> {
695 resolved
696 .map(str::to_string)
697 .or_else(|| match version.trim() {
698 version if version.starts_with("http://") || version.starts_with("https://") => {
699 Some(version.to_string())
700 }
701 _ => None,
702 })
703}
704
705#[allow(clippy::too_many_arguments)]
706fn build_npm_dependency(
707 package_name: &str,
708 version: String,
709 is_dev: bool,
710 is_dev_optional: bool,
711 is_optional: bool,
712 resolved: Option<String>,
713 integrity: Option<&str>,
714 is_direct: bool,
715 from: Option<&str>,
716 in_bundle: bool,
717 nested_deps: Vec<Dependency>,
718) -> Dependency {
719 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
720 let dep_namespace = truncate_field(dep_namespace);
721 let dep_name = truncate_field(dep_name);
722 let (scope, is_runtime, is_optional_flag) =
723 determine_scope(is_dev, is_dev_optional, is_optional);
724
725 let alias_spec = parse_npm_alias_spec(&version);
726 let (purl_namespace, purl_name, resolved_version, is_pinned, dep_purl, download_url) =
727 if let Some((alias_namespace, alias_name, alias_constraint)) = alias_spec.clone() {
728 let alias_namespace = truncate_field(alias_namespace);
729 let alias_name = truncate_field(alias_name);
730 let alias_constraint = truncate_field(alias_constraint);
731 let is_pinned = is_exact_version(&alias_constraint);
732 let dep_purl = create_purl(
733 &alias_namespace,
734 &alias_name,
735 is_pinned.then_some(alias_constraint.as_str()),
736 );
737 let download_url = non_version_download_url(&alias_constraint, resolved.as_deref());
738
739 (
740 alias_namespace,
741 alias_name,
742 alias_constraint,
743 is_pinned,
744 dep_purl,
745 download_url,
746 )
747 } else {
748 let is_pinned = is_exact_version(&version);
749 let dep_purl = create_purl(
750 &dep_namespace,
751 &dep_name,
752 is_pinned.then_some(version.as_str()),
753 );
754 let download_url = non_version_download_url(&version, resolved.as_deref());
755
756 (
757 dep_namespace.clone(),
758 dep_name.clone(),
759 version.clone(),
760 is_pinned,
761 dep_purl,
762 download_url,
763 )
764 };
765
766 let (sha1_from_integrity, sha512_from_integrity) = parse_integrity_field(integrity);
767 let sha1_from_url = resolved.as_deref().and_then(parse_resolved_url);
768 let sha1 = sha1_from_integrity.or(sha1_from_url);
769
770 let mut dep_extra_data = HashMap::new();
771 if let Some(from) = from {
772 dep_extra_data.insert("from".to_string(), Value::String(from.to_string()));
773 }
774 if in_bundle {
775 dep_extra_data.insert("inBundle".to_string(), Value::Bool(true));
776 }
777
778 let resolved_package = ResolvedPackage {
779 primary_language: Some("JavaScript".to_string()),
780 download_url,
781 sha1: sha1.and_then(|h| Sha1Digest::from_hex(&h).ok()),
782 sha256: None,
783 sha512: sha512_from_integrity.and_then(|h| Sha512Digest::from_hex(&h).ok()),
784 md5: None,
785 is_virtual: true,
786 extra_data: None,
787 dependencies: nested_deps,
788 repository_homepage_url: None,
789 repository_download_url: None,
790 api_data_url: None,
791 datasource_id: Some(DatasourceId::NpmPackageLockJson),
792 purl: None,
793 ..ResolvedPackage::new(
794 NpmLockParser::PACKAGE_TYPE,
795 purl_namespace,
796 purl_name,
797 resolved_version,
798 )
799 };
800
801 Dependency {
802 purl: dep_purl,
803 extracted_requirement: Some(truncate_field(version)),
804 scope: Some(scope.to_string()),
805 is_runtime: Some(is_runtime),
806 is_optional: Some(is_optional_flag),
807 is_pinned: Some(is_pinned),
808 is_direct: Some(is_direct),
809 resolved_package: Some(Box::new(resolved_package)),
810 extra_data: (!dep_extra_data.is_empty()).then_some(dep_extra_data),
811 }
812}
813
814fn build_link_dependency(
815 package_name: &str,
816 is_dev: bool,
817 is_dev_optional: bool,
818 is_optional: bool,
819 resolved: Option<String>,
820 is_direct: bool,
821) -> Dependency {
822 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
823 let dep_namespace = truncate_field(dep_namespace);
824 let dep_name = truncate_field(dep_name);
825 let (scope, is_runtime, is_optional_flag) =
826 determine_scope(is_dev, is_dev_optional, is_optional);
827 let mut extra_data = HashMap::from([("link".to_string(), Value::Bool(true))]);
828
829 if let Some(resolved) = &resolved {
830 extra_data.insert("resolved".to_string(), Value::String(resolved.clone()));
831 }
832
833 Dependency {
834 purl: create_purl(&dep_namespace, &dep_name, None),
835 extracted_requirement: resolved.map(truncate_field),
836 scope: Some(scope.to_string()),
837 is_runtime: Some(is_runtime),
838 is_optional: Some(is_optional_flag),
839 is_pinned: Some(false),
840 is_direct: Some(is_direct),
841 resolved_package: None,
842 extra_data: Some(extra_data),
843 }
844}
845
846crate::register_parser!(
847 "npm package-lock.json lockfile",
848 &[
849 "**/package-lock.json",
850 "**/.package-lock.json",
851 "**/npm-shrinkwrap.json"
852 ],
853 "npm",
854 "JavaScript",
855 Some("https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json"),
856);