Skip to content

Commit 0b3626c

Browse files
authored
Corrected timestamps and reverted float casting. #294 #292 (#295)
* Pushed all times to UTC. added debug flag. #292 * Additional corrections to timestamps. - Corrected issue with comparing naive timestamps with aware timestamps. - Added mapping DB cleanup to sync * Reverting casting float to string #294 * Updated testing suite
1 parent dc458e0 commit 0b3626c

File tree

11 files changed

+151
-62
lines changed

11 files changed

+151
-62
lines changed

pyproject.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,25 @@ readme = {file = ["README.md"], content-type = "text/markdown"}
5555
[tool.setuptools.packages.find]
5656
include = ["tenb2jira*"]
5757

58+
[tool.ruff]
59+
target-version = "py312"
60+
exclude = [
61+
".nova",
62+
".github",
63+
".git",
64+
".pytest_cache",
65+
"__pycache__"
66+
]
67+
68+
[tool.ruff.lint]
69+
select = ["E4", "E7", "E9", "F", "B"]
70+
fixable = [ "ALL" ]
71+
unfixable = [ "B" ]
72+
73+
[tool.ruff.lint.per-file-ignores]
74+
"__init__.py" = ["E402"]
75+
"**/{tests,docs,tools}/*" = ["E402"]
76+
77+
[tool.flake8]
78+
max-line-length = 88
79+
count = true

tenb2jira/cli.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,26 @@ def sync(configfile: Path,
129129
verbose: bool = False,
130130
cleanup: bool = True,
131131
ignore_last_run: bool = False,
132+
debug: bool = False,
132133
):
133134
"""
134135
Perform the sync between Tenable & Jira
135136
"""
136-
setup_logging(verbose)
137137
with configfile.open('r', encoding='utf-8') as fobj:
138138
config = tomlkit.load(fobj)
139+
140+
if debug:
141+
verbose = True
142+
cleanup = False
143+
config['jira']['max_workers'] = 1
144+
145+
setup_logging(verbose)
146+
147+
dbfile = Path(config['mapping_database']['path'])
148+
if dbfile.exists():
149+
console.print('WARNING :: Mapping Cache discovered. We will be removing it.')
150+
dbfile.unlink()
151+
139152
processor = Processor(config, ignore_last_run=ignore_last_run)
140153
console.print(Columns([tenable_table(config),
141154
jira_table(config)

tenb2jira/jira/field.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ def __repr__(self):
5959

6060
@property
6161
def attr(self):
62-
if self.attribute:
63-
return self.attribute
64-
if self.static_value:
65-
return self.attribute
62+
"""
63+
Return the appropriate value (either platform_id, static_value, or
64+
attribute) depending on how the field was configured.
65+
"""
6666
if self.platform_id:
6767
return self.platform_id
68+
if self.static_value:
69+
return self.static_value
70+
return self.attribute
6871

6972
def fetch_field_id(self, api) -> bool:
7073
"""
@@ -155,7 +158,7 @@ def parse_value(self, finding: dict) -> Any:
155158

156159
# float values should always be returned as a float.
157160
case 'float':
158-
return str(float(value))
161+
return float(value)
159162

160163
# datetime values should be returned in a specific format. Here
161164
# we attempt to normalize both timestamp and ISO formatted values

tenb2jira/jira/jira.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Dict
22
import time
33
import logging
44
from box import Box
@@ -24,12 +24,18 @@ class Jira:
2424
project: dict
2525

2626
@property
27-
def field_by_name_map(self):
28-
return {f.name:f for f in self.fields}
27+
def field_by_name_map(self) -> Dict[str, Field]:
28+
"""
29+
Returns the fields in a dictionary with the field name as the key
30+
"""
31+
return {f.name: f for f in self.fields}
2932

3033
@property
31-
def field_by_id_map(self):
32-
return {f.id:f for f in self.fields}
34+
def field_by_id_map(self) -> Dict[str, Field]:
35+
"""
36+
Returns the fields in a dictionary with the field id as the key
37+
"""
38+
return {f.id: f for f in self.fields}
3339

3440
def __init__(self, config: dict):
3541
self.config = config

tenb2jira/jira/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, issue_def: "Task", is_open: bool = True):
2525
self.is_open = is_open
2626

2727
def __repr__(self):
28-
return f'Task("{self.jql}", {len(self.fields)})'
28+
return f'Task("{self.jql_stmt}", {len(self.fields)})'
2929

3030
@property
3131
def jql_stmt(self):

tenb2jira/processor.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Optional
12
import logging
23
from concurrent.futures import ThreadPoolExecutor
34
from pathlib import Path
@@ -29,8 +30,13 @@ class Processor:
2930
plugin_id: str
3031
closed_map: list[str]
3132

32-
def __init__(self, config: dict, ignore_last_run: bool = False):
33-
dburi = f'sqlite:///{config["mapping_database"].get("path")}'
33+
def __init__(self,
34+
config: dict,
35+
ignore_last_run: bool = False,
36+
dburi: Optional[str] = None,
37+
):
38+
if not dburi:
39+
dburi = f'sqlite:///{config["mapping_database"].get("path")}'
3440

3541
# For situations where we will need to ignore the last_run variable,
3642
# This will pull it from the config, forcing the integration to use
@@ -124,7 +130,7 @@ def build_mapping_db_model(self,
124130
value = value[0]
125131
item[fields[key]] = value
126132
# item = {fields[k]: v for k, v in issue.fields.items()}
127-
item['updated'] = self.start_time
133+
item['updated'] = self.start_time.datetime
128134
item['jira_id'] = issue.key
129135
if not missing:
130136
log.debug(f'Adding {issue.key} to cache.')
@@ -185,11 +191,11 @@ def upsert_task(self, s: Session, finding: dict) -> (int | None):
185191
# and return the jira issue id back to the caller.
186192
if sql:
187193
if finding.get('integration_pid_updated') > self.last_run:
188-
if sql.updated <= self.start_time:
194+
if arrow.get(sql.updated, 'UTC') <= self.start_time:
189195
self.jira.api.issues.update(sql.jira_id,
190196
fields=task.fields,
191197
)
192-
sql.updated = datetime.now()
198+
sql.updated = datetime.utcnow()
193199
s.commit()
194200
log.info(f'Matched Task "{sql.jira_id}" to '
195201
'SQL Cache and updated.')
@@ -213,7 +219,7 @@ def upsert_task(self, s: Session, finding: dict) -> (int | None):
213219
if len(page.issues) == 1:
214220
sql = TaskMap(plugin_id=task.fields[self.plugin_id],
215221
jira_id=page.issues[0].key,
216-
updated=datetime.now(),
222+
updated=datetime.utcnow(),
217223
)
218224
s.add(sql)
219225
s.commit()
@@ -232,7 +238,7 @@ def upsert_task(self, s: Session, finding: dict) -> (int | None):
232238
resp = self.jira.api.issues.create(fields=task.fields)
233239
sql = TaskMap(plugin_id=task.fields[self.plugin_id],
234240
jira_id=resp.key,
235-
updated=datetime.now()
241+
updated=datetime.utcnow()
236242
)
237243
s.add(sql)
238244
s.commit()
@@ -271,7 +277,7 @@ def upsert_subtask(self,
271277
if sql:
272278
if not task.is_open:
273279
sql.is_open = task.is_open
274-
sql.updated = datetime.now()
280+
sql.updated = datetime.utcnow()
275281
s.commit()
276282
self.close_task(sql.jira_id)
277283
action = 'closed subtask'
@@ -313,7 +319,7 @@ def upsert_subtask(self,
313319
finding_id=task.fields[self.finding_id],
314320
jira_id=page.issues[0].key,
315321
is_open=task.is_open,
316-
updated=datetime.now(),
322+
updated=datetime.utcnow(),
317323
)
318324
s.add(sql)
319325
s.commit()
@@ -341,7 +347,7 @@ def upsert_subtask(self,
341347
finding_id=task.fields[self.finding_id],
342348
jira_id=resp.key,
343349
is_open=task.is_open,
344-
updated=datetime.now(),
350+
updated=datetime.utcnow(),
345351
)
346352
s.add(sql)
347353
s.commit()
@@ -419,11 +425,10 @@ def sync(self, cleanup: bool = True):
419425
"""
420426
Tenable to Jira Synchronization method.
421427
"""
422-
self.start_time = datetime.now()
423-
ts = int(arrow.get(self.start_time).timestamp())
428+
self.start_time = arrow.utcnow()
424429

425430
# Get the findings and the asset cleanup generators.
426-
findings = self.tenable.get_generator()
431+
findings = self.tenable.get_generator(self.start_time)
427432
asset_cleanup = self.tenable.get_asset_cleanup()
428433

429434
# build the db cache
@@ -469,8 +474,8 @@ def sync(self, cleanup: bool = True):
469474
self.close_empty_tasks()
470475

471476
# update the last_run timestamp with the time that we started the sync.
472-
self.config['tenable']['last_run'] = ts
473-
self.finished_time = datetime.now()
477+
self.config['tenable']['last_run'] = int(self.start_time.timestamp())
478+
self.finished_time = arrow.utcnow()
474479

475480
self.engine.dispose()
476481
# Delete the mapping database.

tenb2jira/tenable/tenable.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010

1111
class Tenable:
12-
tvm: (TenableIO | None) = None
13-
tsc: (TenableSC | None) = None
12+
tvm: TenableIO
13+
tsc: TenableSC
1414
config: dict
1515
platform: str
1616
timestamp: int
@@ -82,11 +82,10 @@ def get_tvm_generator(self) -> Generator[Any, Any, Any]:
8282
close_accepted=self.close_accepted,
8383
)
8484

85-
def get_tsc_generator(self) -> Generator[Any, Any, Any]:
85+
def get_tsc_generator(self, start_time: int) -> Generator[Any, Any, Any]:
8686
"""
8787
Queries the Analysis API and returns the TSC Generator.
8888
"""
89-
self.last_run = int(arrow.now().timestamp())
9089

9190
# The severity map to link the string severities to the integer values
9291
# that TSC expects.
@@ -99,7 +98,7 @@ def get_tsc_generator(self) -> Generator[Any, Any, Any]:
9998
}
10099

101100
# Construct the TSC timestamp offsets.
102-
tsc_ts = f'{self.timestamp}-{self.last_run}'
101+
tsc_ts = f'{self.timestamp}-{start_time}'
103102

104103
# The base parameters to pass to the API.
105104
params = {
@@ -136,14 +135,15 @@ def get_tsc_generator(self) -> Generator[Any, Any, Any]:
136135
close_accepted=self.close_accepted,
137136
)
138137

139-
def get_generator(self) -> (Generator[Any, Any, Any] | None):
138+
def get_generator(self,
139+
start_time: arrow.Arrow
140+
) -> Generator[Any, Any, Any]:
140141
"""
141142
Retreives the appropriate generator based on the configured platform.
142143
"""
143144
if self.platform == 'tvm':
144145
return self.get_tvm_generator()
145-
if self.platform == 'tsc':
146-
return self.get_tsc_generator()
146+
return self.get_tsc_generator(int(start_time.timestamp()))
147147

148148
def get_asset_cleanup(self) -> (Generator[Any, Any, Any] | list):
149149
if self.platform == 'tvm':
@@ -154,5 +154,4 @@ def get_asset_cleanup(self) -> (Generator[Any, Any, Any] | list):
154154
chunk_size=self.chunk_size
155155
)
156156
return tvm_asset_cleanup(dassets, tassets)
157-
else:
158-
return []
157+
return []

tenb2jira/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = '2.0.9'
1+
version = '2.0.10'

tests/jira/test_field.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,43 @@ def field_config():
1818
}
1919

2020

21+
def test_field_repr(field_config):
22+
f = Field(config=field_config,
23+
platform='tvm',
24+
platform_map={'tvm': 'Test Platform'}
25+
)
26+
assert repr(f) == 'Field(1: Test Field)'
27+
28+
29+
def test_field_attr(field_config):
30+
f = Field(config=field_config,
31+
platform='tvm',
32+
platform_map={'tvm': 'Test Platform'}
33+
)
34+
assert f.attr == 'test'
35+
36+
field_config.pop('attr', None)
37+
field_config['platform_id'] = True
38+
f = Field(config=field_config,
39+
platform='tvm',
40+
platform_map={'tvm': 'Test Platform'}
41+
)
42+
assert f.attribute == None
43+
assert f.platform_id == 'Test Platform'
44+
assert f.attr == 'Test Platform'
45+
46+
field_config.pop('platform_id', None)
47+
field_config['static_value'] = 'static'
48+
f = Field(config=field_config,
49+
platform='tvm',
50+
platform_map={'tvm': 'Test Platform'}
51+
)
52+
assert f.attribute == None
53+
assert f.platform_id == None
54+
assert f.static_value == 'static'
55+
assert f.attr == 'static'
56+
57+
2158
def test_field_noapi(field_config):
2259
f = Field(config=field_config,
2360
platform='tvm',
@@ -130,9 +167,9 @@ def test_field_parse_value_float(field_config):
130167
platform_map={'tvm': 'Test Platform'}
131168
)
132169
f.type = 'float'
133-
assert f.parse_value({'test': 1}) == '1.0'
134-
assert f.parse_value({'test': 1.0}) == '1.0'
135-
assert f.parse_value({'test': '1'}) == '1.0'
170+
assert f.parse_value({'test': 1}) == 1.0
171+
assert f.parse_value({'test': 1.0}) == 1.0
172+
assert f.parse_value({'test': '1'}) == 1.0
136173

137174

138175
def test_field_parse_value_datetime(field_config):

0 commit comments

Comments
 (0)