Skip to content

Commit 9a31374

Browse files
committed
Map unmatched node_modules files
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 9fc98a8 commit 9a31374

5 files changed

Lines changed: 138 additions & 0 deletions

File tree

scanpipe/pipelines/deploy_to_develop.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def steps(cls):
5454
cls.map_javascript,
5555
cls.match_purldb,
5656
cls.map_javascript_post_purldb_match,
57+
cls.map_javascript_npm_lookup,
5758
cls.map_javascript_path,
5859
cls.map_path,
5960
cls.flag_mapped_resources_and_ignored_directories,
@@ -160,6 +161,14 @@ def map_javascript_post_purldb_match(self):
160161
"""Map minified javascript file based on existing PurlDB match."""
161162
d2d.map_javascript_post_purldb_match(project=self.project, logger=self.log)
162163

164+
def map_javascript_npm_lookup(self):
165+
"""Map unmatched ``node_modules`` files."""
166+
if not purldb.is_available():
167+
self.log("PurlDB is not available. Skipping.")
168+
return
169+
170+
d2d.map_javascript_npm_lookup(project=self.project, logger=self.log)
171+
163172
def map_javascript_path(self):
164173
"""Map javascript file based on path."""
165174
d2d.map_javascript_path(project=self.project, logger=self.log)

scanpipe/pipes/d2d.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from scanpipe import pipes
3030
from scanpipe.models import CodebaseRelation
3131
from scanpipe.models import CodebaseResource
32+
from scanpipe.models import DiscoveredPackage
3233
from scanpipe.pipes import flag
3334
from scanpipe.pipes import get_resource_diff_ratio
3435
from scanpipe.pipes import js
@@ -840,3 +841,100 @@ def _map_javascript_path_resource(
840841
extra_data=extra_data,
841842
)
842843
return len(transpiled)
844+
845+
846+
def map_javascript_npm_lookup(project, logger=None):
847+
"""Map unmatched ``node_modules`` files."""
848+
project_directories = project.codebaseresources.directories().only("path")
849+
project_files = project.codebaseresources.files().only("path")
850+
851+
to_directories_key = (
852+
project_directories.to_codebase()
853+
.no_status()
854+
.filter(path__regex=r"^.*\/node_modules\/(?!.*\/)")
855+
.distinct()
856+
)
857+
858+
to_resources = (
859+
project_files.to_codebase()
860+
.no_status()
861+
.filter(path__regex=r"^.*\/node_modules\/.*$")
862+
)
863+
864+
if not to_directories_key:
865+
logger("No unmatched ``node_modules`` file is available. Skipping.")
866+
return
867+
868+
resource_count = to_resources.count()
869+
870+
if logger:
871+
logger(
872+
f"Mapping {resource_count:,d} to/ resources using javascript map "
873+
f"against from/ codebase"
874+
)
875+
876+
to_resources_index = pathmap.build_index(
877+
to_resources.values_list("id", "path"), with_subpaths=True
878+
)
879+
880+
resource_iterator = to_directories_key.iterator(chunk_size=2000)
881+
last_percent = 0
882+
map_count = 0
883+
start_time = timer()
884+
885+
for resource_index, to_directory in enumerate(resource_iterator):
886+
last_percent = pipes.log_progress(
887+
logger,
888+
resource_index,
889+
resource_count,
890+
last_percent,
891+
increment_percent=10,
892+
start_time=start_time,
893+
)
894+
map_count += _map_javascript_npm_lookup_resource(
895+
to_directory,
896+
to_resources,
897+
to_resources_index,
898+
project,
899+
)
900+
901+
logger(f"{map_count:,d} resource(s) mapped")
902+
903+
904+
def _map_javascript_npm_lookup_resource(
905+
to_directory,
906+
to_resources,
907+
to_resources_index,
908+
project,
909+
):
910+
"""Map unmatched ``node_modules`` files."""
911+
purl = js.get_purl_from_node_module(to_directory)
912+
matched = to_resources.filter(path__startswith=to_directory.path)
913+
if not matched:
914+
return 0
915+
916+
try:
917+
package = project.discoveredpackages.get(
918+
type=purl.type,
919+
namespace="" if not purl.namespace else purl.namespace,
920+
name=purl.name,
921+
version=purl.version,
922+
)
923+
package.add_resources(matched)
924+
925+
except DiscoveredPackage.DoesNotExist:
926+
if results := purldb.fetch_package(purl=str(purl)):
927+
package_data = results[0]
928+
package_data.pop("uuid", None)
929+
package_data.pop("dependencies", None)
930+
931+
package = pipes.update_or_create_package(
932+
project=project,
933+
package_data=package_data,
934+
codebase_resources=matched,
935+
)
936+
else:
937+
return 0
938+
939+
matched.update(status=flag.NPM_LOOKUP)
940+
return len(matched)

scanpipe/pipes/flag.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
MATCHED_TO_PURLDB = "matched-to-purldb"
5050
TOO_MANY_MAPS = "too-many-maps"
5151
NO_JAVA_SOURCE = "no-java-source"
52+
NPM_LOOKUP = "npm-lookup"
5253

5354

5455
def flag_empty_files(project):

scanpipe/pipes/js.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import json
2525
from pathlib import Path
2626

27+
from packageurl import PackageURL
28+
2729
from scanpipe.pipes import get_text_str_diff_ratio
2830
from scanpipe.pipes import pathmap
2931

@@ -204,3 +206,21 @@ def get_js_map_basename_and_extension(filename):
204206
if filename.endswith(ext):
205207
basename = filename[: -len(ext)]
206208
return basename, ext
209+
210+
211+
def get_purl_from_node_module(node_module_directory):
212+
"""Return PURL for given a `node_modules` package directory."""
213+
path = Path(node_module_directory.path)
214+
path_parts = path.parts
215+
216+
npm_package = path_parts[-1]
217+
218+
if "$" in npm_package:
219+
_, npm_package = npm_package.split("$")
220+
221+
# Handle the scoped pacakage.
222+
if "%2F" in npm_package:
223+
npm_package = npm_package.replace("%2F", "/")
224+
npm_package = f"%40{npm_package}"
225+
226+
return PackageURL.from_string(f"pkg:npm/{npm_package}")

scanpipe/pipes/purldb.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,13 @@ def submit_purls(purls, timeout=None, api_url=PURLDB_API_URL):
127127
)
128128

129129
return response
130+
131+
132+
def fetch_package(purl, timeout=None, api_url=PURLDB_API_URL):
133+
"""Fetch package data for the PURL."""
134+
payload = {"purl": purl}
135+
response = request_get(url=f"{api_url}packages/", payload=payload, timeout=timeout)
136+
137+
if response and response.get("count"):
138+
results = response["results"]
139+
return results

0 commit comments

Comments
 (0)