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