Skip to content

Commit 1750f64

Browse files
committed
- add download and mirror cache mechanism
- add `--download-only` option to `aura scan` - various bugfixes
1 parent a6b09e7 commit 1750f64

File tree

19 files changed

+202
-33
lines changed

19 files changed

+202
-33
lines changed

aura/analyzers/python/nodes.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import typing
88
import inspect
9+
import logging
910
from abc import ABCMeta, abstractmethod
1011
from enum import Enum
1112
from pathlib import Path
@@ -26,6 +27,8 @@
2627
str,
2728
int,
2829
)
30+
logger = logging.getLogger(__name__)
31+
2932

3033
@total_ordering
3134
class Taints(Enum):
@@ -231,12 +234,15 @@ def pprint(self):
231234
from prettyprinter import pprint as pp
232235
pp(self)
233236

234-
def add_taint(self, taint: Taints, context: Context, lock=False, taint_log=None) -> bool:
237+
def add_taint(self, taint: Taints, context: Context, lock=False, taint_log: typing.Optional[TaintLog]=None) -> bool:
235238
"""
236239
Assign a taint to the node
237240
Operation is ignored if the current taint is already higher or equal
238241
return True if the taint was modified (increased)
239242
"""
243+
if taint_log:
244+
logger.debug(f"{taint_log.message} at {taint_log.path}:{taint_log.line_no}")
245+
240246
if lock:
241247
self._taint_locked = True
242248
if taint != self._taint_class:

aura/analyzers/python/readonly.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def __init__(self, *, location: ScanLocation):
1717
def __call__(self) -> typing.Generator[Detection, None, None]:
1818
if not self.hooks:
1919
return
20-
elif self.location.is_python_source_code:
20+
elif not self.location.is_python_source_code:
2121
return
2222
try:
2323
for x in self.hooks:

aura/cache.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import os
2+
import shutil
3+
import hashlib
4+
import logging
5+
from pathlib import Path
6+
from typing import Optional
7+
8+
from . import utils
9+
from . import config
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class Cache:
15+
DISABLE_CACHE = bool(os.environ.get("AURA_NO_CACHE"))
16+
__location: Optional[Path] = None
17+
18+
@classmethod
19+
def get_location(cls) -> Optional[Path]:
20+
if cls.DISABLE_CACHE:
21+
return None
22+
23+
if cls.__location is None:
24+
c = os.environ.get("AURA_CACHE_LOCATION") or config.CFG["aura"].get("cache_location")
25+
if c:
26+
c = Path(c).expanduser().resolve()
27+
logger.debug(f"Cache location set to {c}")
28+
29+
if not c.exists():
30+
c.mkdir(parents=True)
31+
cls.__location = c
32+
33+
return cls.__location
34+
35+
@classmethod
36+
def purge_cache(cls): # TODO
37+
total, used, free = shutil.disk_usage(cls.get_location())
38+
cache_items = [x for x in cls.get_location().iterdir()]
39+
cache_items.sort(key=lambda x: x.stat().st_mtime)
40+
41+
@classmethod
42+
def proxy_url(cls, *, url, fd, cache_id=None):
43+
if cls.get_location() is None:
44+
return utils.download_file(url, fd=fd)
45+
46+
if cache_id is None:
47+
cache_id = hashlib.md5(url).hexdigest()
48+
49+
cache_id = f"url_{cache_id}"
50+
cache_pth: Path = cls.get_location()/cache_id
51+
52+
if cache_pth.is_file():
53+
logger.info(f"Loading {cache_id} from cache")
54+
with cache_pth.open("rb") as cfd:
55+
shutil.copyfileobj(cfd, fd)
56+
return
57+
58+
try:
59+
with cache_pth.open("wb") as cfd:
60+
utils.download_file(url, cfd)
61+
cfd.flush()
62+
with cache_pth.open("rb") as cfd:
63+
shutil.copyfileobj(cfd, fd)
64+
except Exception as exc:
65+
cache_pth.unlink(missing_ok=True)
66+
raise exc
67+
68+
@classmethod
69+
def proxy_mirror(cls, *, src: Path, cache_id=None):
70+
if not src.exists():
71+
return None
72+
elif cls.get_location() is None:
73+
return src
74+
75+
if cache_id is None:
76+
cache_id = src.name
77+
78+
cache_id = f"mirror_{cache_id}"
79+
cache_pth: Path = cls.get_location() / cache_id
80+
81+
try:
82+
if not cache_pth.exists():
83+
with cache_pth.open("wb") as cfd:
84+
with src.open("rb") as fd:
85+
shutil.copyfileobj(fd, cfd)
86+
cfd.flush()
87+
return cache_pth
88+
except Exception as exc:
89+
cache_pth.unlink(missing_ok=True)
90+
raise exc

aura/cli.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def cli(ctx, **kwargs):
6969
)
7070
@click.option("--async", "fork_mode", flag_value=True)
7171
@click.option("--no-async", "fork_mode", flag_value=False)
72+
@click.option("--download-only", "download_only", flag_value=True)
7273
def scan(
7374
uri,
7475
verbose=None,
@@ -78,7 +79,8 @@ def scan(
7879
benchmark=False,
7980
benchmark_sort="cumtime",
8081
filter_tags=None,
81-
fork_mode=False
82+
fork_mode=False,
83+
download_only=False,
8284
):
8385
output_opts = {
8486
"tags": filter_tags
@@ -109,7 +111,7 @@ def scan(
109111
cProfile, pstats, pr, io = None, None, None, None
110112

111113
try:
112-
commands.scan_uri(uri, metadata=meta)
114+
commands.scan_uri(uri, metadata=meta, download_only=download_only)
113115
except exceptions.FeatureDisabled as e:
114116
LOGGER.exception(e.args[0], exc_info=e)
115117
click.secho(e.args[0], err=True, fg="red")

aura/commands.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import traceback
88
from pathlib import Path
99
from functools import partial
10-
from typing import Union, Optional, Tuple, Generator
10+
from typing import Union, Optional, Tuple, Generator, List
1111

1212
import click
1313
from prettyprinter import pprint
@@ -42,8 +42,6 @@ def check_requirement(pkg):
4242
"format": "plain",
4343
"min_score": 0,
4444
}
45-
46-
4745
hits = []
4846

4947
for location in handler.get_paths(metadata=metadata):
@@ -75,7 +73,7 @@ def scan_worker(item: ScanLocation) -> Generator[Detection, None, None]:
7573
yield from sandbox.run()
7674

7775

78-
def scan_uri(uri, metadata: Union[list, dict]=None) -> list:
76+
def scan_uri(uri, metadata: Union[list, dict]=None, download_only: bool=False) -> List[Detection]:
7977
with utils.enrich_exception(uri, metadata):
8078
start = time.time()
8179
handler = None
@@ -105,7 +103,10 @@ def scan_uri(uri, metadata: Union[list, dict]=None) -> list:
105103

106104
# FIXME: metadata=metadata
107105
for x in handler.get_paths(metadata={"analyzers": metadata["analyzers"]}): # type: ScanLocation
108-
all_hits.extend(scan_worker(x))
106+
if download_only:
107+
continue
108+
else:
109+
all_hits.extend(scan_worker(x))
109110

110111
for formatter in formatters:
111112
try:

aura/data/aura_config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ aura: &aura_config
66
# https://docs.python.org/3.7/library/warnings.html#overriding-the-default-filter
77
warnings: default
88

9+
# Directory location for caching, unset to disable caching
10+
cache_location: &cache_location ~/.aura_cache
11+
912
# Path to the location of the offline PyPI mirror
1013
# This option can be safely disabled if you don't have a local mirror, some advanced features require this
1114
mirror: /var/pypi_mirror/pypi/web/

aura/diff.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ class Diff():
5757

5858
def __post_init__(self):
5959
assert self.operation in ("A", "D", "M", "R")
60+
61+
if self.a_mime == "text/x-script.python":
62+
self.a_mime = "text/x-python"
63+
64+
if self.b_mime == "text/x-script.python":
65+
self.b_mime = "text/x-python"
66+
6067
# a_path, b_path = self.a_path, self.b_path
6168
#
6269
# if a_path and b_path:

aura/output/text.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import os
23
import sys
34
from shutil import get_terminal_size
45
from dataclasses import dataclass
@@ -55,6 +56,8 @@
5556
OK = '\u2713'
5657
NOK = '\u2717'
5758

59+
TTY_COLORS = bool(os.environ.get("AURA_FORCE_COLORS", False)) or None
60+
5861

5962

6063
class PrettyReport:
@@ -81,7 +84,7 @@ def ansi_length(cls, line:str):
8184
return len(cls.ANSI_RE.sub("", line))
8285

8386
def print_separator(self, sep="\u2504", left="\u251C", right="\u2524"):
84-
secho(f"{left}{sep*(self.width-2)}{right}", file=self.fd)
87+
secho(f"{left}{sep*(self.width-2)}{right}", file=self.fd, color=TTY_COLORS)
8588

8689
def print_thick_separator(self):
8790
self.print_separator(left="\u255E", sep="\u2550", right="\u2561")
@@ -96,11 +99,11 @@ def print_heading(self, text, left="\u251C", right="\u2524", infill="\u2591"):
9699
text_len = self.ansi_length(text)
97100
ljust = (self.width-4-text_len)//2
98101
rjust = self.width-4-text_len-ljust
99-
secho(f"{left}{infill*ljust} {text} {infill*rjust}{right}", file=self.fd)
102+
secho(f"{left}{infill*ljust} {text} {infill*rjust}{right}", file=self.fd, color=TTY_COLORS)
100103

101104
def align(self, line, pos=-1, left="\u2502 ", right=" \u2502"):
102105
line = self._align_text(line, self.width - len(left) - len(right), pos=pos)
103-
secho(f"{left}{line}{right}", file=self.fd)
106+
secho(f"{left}{line}{right}", file=self.fd, color=TTY_COLORS)
104107

105108
def wrap(self, text, left="\u2502 ", right=" \u2502"):
106109
remaining_len=self.width - len(left) - len(right)
@@ -271,7 +274,7 @@ def output(self, hits, scan_metadata: dict):
271274
if score < self.min_score:
272275
return
273276

274-
secho("\n", file=self._fd) # Empty line for readability
277+
secho("\n", file=self._fd, color=TTY_COLORS) # Empty line for readability
275278
self._formatter.print_top_separator()
276279
self._formatter.print_heading(style(f"Scan results for {scan_metadata.get('name', 'N/A')}", fg="bright_green"))
277280
score_color = "bright_green" if score == 0 else "bright_red"

aura/package.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from . import config
2323
from . import github
24+
from . import cache
2425
from .json_proxy import loads
2526
from . import utils
2627
from . import exceptions
@@ -109,7 +110,7 @@ def download_release(
109110

110111
for url in filtered:
111112
with open(dest / url["filename"], "wb") as fd:
112-
utils.download_file(url["url"], fd)
113+
cache.Cache.proxy_url(url=url["url"], fd=fd, cache_id=url["filename"])
113114
files.append(url)
114115

115116
return files

aura/pattern_matching.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import inspect
55
import fnmatch
66
import string
7+
import logging
78
from abc import ABCMeta, abstractmethod
89
from typing import List, Mapping
910
from functools import lru_cache
@@ -16,6 +17,7 @@
1617

1718

1819
PATTERN_CACHE = None
20+
logger = logging.getLogger(__name__)
1921

2022

2123
class PatternMatcher(metaclass=ABCMeta):
@@ -272,6 +274,7 @@ class AnyOf(list):
272274

273275

274276
def __init__(self, signature: dict):
277+
self._id = None
275278
self._signature = signature
276279

277280
if type(self._signature["pattern"]) == str:
@@ -290,16 +293,21 @@ def _compile_src(cls, src: str) -> nodes.NodeType:
290293

291294
@property
292295
def id(self) -> str:
293-
if "id" in self._signature:
294-
return self._signature["id"]
296+
if self._id is None:
297+
if "id" in self._signature:
298+
return self._signature["id"]
295299

296-
chars = string.ascii_letters + string.digits + "._-"
297-
return "".join(x for x in self._signature["pattern"] if x in chars)
300+
chars = string.ascii_letters + string.digits + "._-"
301+
self._id = "".join(x for x in self._signature["pattern"] if x in chars)
302+
303+
return self._id
298304

299305
def match(self, node: nodes.ASTNode) -> bool:
300306
return self.ctx.match(node, self._compiled)
301307

302308
def apply(self, context: nodes.Context):
309+
logger.debug(f"Applying AST pattern {self.id} at {context.node.line_no}")
310+
303311
if "tags" in self._signature:
304312
context.node.tags |= set(self._signature["tags"])
305313

0 commit comments

Comments
 (0)