Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 94 additions & 22 deletions src/fosslight_binary/binary_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@
import subprocess
import re
import shutil
import tempfile

PKG_NAME = "fosslight_binary"
logger = logging.getLogger(constant.LOGGER_NAME)

_REMOVE_FILE_EXTENSION = ['json', 'js']
_REMOVE_FILE_EXTENSION = ['json', 'js', 'xlsx', 'xls', 'xlsm']
Comment thread
soimkim marked this conversation as resolved.
_REMOVE_FILE_COMMAND_RESULT = ['timezone data', 'apple binary property list']
INCLUDE_FILE_COMMAND_RESULT = ['current ar archive']
_TEMP_DIR_PREFIX = '.fosslight_temp_'
_LOG_FILE_PREFIX = 'fosslight_log_bin_'

_error_logs = []
_root_path = ""
Expand Down Expand Up @@ -71,7 +74,67 @@ def get_checksum_and_tlsh(bin_with_path):
return checksum_value, tlsh_value, error_msg


def init(path_to_find_bin, output_file_name, formats, path_to_exclude=[]):
def _prepare_temp_dir(output_dir, file_time):
"""Create the temp directory that holds intermediate output.

Keep the timestamp in the directory name for traceability, but let the
OS add a unique suffix so concurrent scans targeting the same output path
never clobber each other's intermediate files.
"""
os.makedirs(output_dir, exist_ok=True)
return tempfile.mkdtemp(prefix=f'{_TEMP_DIR_PREFIX}{file_time}_', dir=output_dir)


def _cleanup_temp_dir(temp_path):
"""Remove the temp directory if it is still there.

Called from the finally block of find_binaries so that only this
invocation's directory is removed, however the analysis ends.
"""
if not temp_path or not os.path.isdir(temp_path):
return

temp_path = os.path.abspath(temp_path)
logging_logger = logging.getLogger(constant.LOGGER_NAME)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가 확인 사항: _cleanup_temp_dir는 constant.LOGGER_NAME 로거에 직접 붙은 FileHandler만 닫음. 그런데 init 이후 모듈 전역 logger는 init_log의 반환 값으로 재할당되므로, init_log가 다른 이름의 로거에 FileHandler를 붙인다면 핸들러가 닫히지 않아 Windows에서 rmtree(ignore_errors=True)가 조용히 실패하고 temp가 남을 수 있음. init_log 구현에서 두 로거 이름이 일치하는지 확인 필요

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbit.ai, logger가 전역 변수로 있는데 logging_logger를 따로 받을 필요가 있나?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soimkim, 별도의 logging_logger는 필요하지 않습니다.

init()는 init_log()의 반환값으로 전역 logger를 재할당합니다. 따라서 _cleanup_temp_dir()에서는 전역 logger.handlers[:]를 사용해야 합니다. 그러면 init_log()가 실제로 파일 핸들러를 연결한 로거에서 핸들러를 닫습니다.

logging.getLogger(constant.LOGGER_NAME)를 다시 호출하면, init_log()가 다른 이름의 로거를 반환한 경우 대상 로거가 달라질 수 있습니다. 이 경우 파일 핸들러가 열린 상태로 남을 수 있습니다.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjk7119 , 기존 전역 logger를 사용하는 것을 권장드립니다.

for handler in logging_logger.handlers[:]:
if (isinstance(handler, logging.FileHandler)
and os.path.dirname(os.path.abspath(handler.baseFilename)) == temp_path):
handler.flush()
handler.close()
logging_logger.removeHandler(handler)

shutil.rmtree(temp_path, ignore_errors=True)


def _finalize_temp_output(temp_output_path, final_output_path, log_file, file_time, log=None):
"""Publish analysis artifacts from the temp directory."""
if not temp_output_path or not os.path.isdir(temp_output_path):
return True

publish_ok = True
try:
if os.path.isfile(log_file):
move_log_file(log_file, os.path.join(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가 확인 사항: Windows에서는 FileHandler가 로그 파일을 열고 있는 상태에서 move_log_file이 호출되어 이동이 실패할 수 있음(열린 파일은 rename 불가). 이 경우 publish_ok=False가 되어 copytree가 실제로 결과를 게시하더라도 success_to_write=False로 반환됨. 로그 이동 전에 핸들러를 먼저 닫는 헬퍼를 호출하도록 순서 조정 권장

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjk7119 , windows에서 정상 동작하는지 테스트 결과 공유 부탁드립니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문제: _finalize_temp_output가 로그 파일을 move할 시점에 해당 파일의 FileHandler가 아직 닫혀 있지 않을 수 있습니다. 핸들러를 닫는 로직은 그 이후 단계인 _cleanup_temp_dir에서만 수행되므로 move 시점에는 도움이 되지 않습니다. Windows에서는 열려 있는 파일의 이동이 PermissionError로 실패하기 쉬운데, 이번 변경부터 move 실패가 publish_ok=False → success_to_write=False로 승격되어 정상 완료된 스캔의 반환값(및 CLI exit code)이 실패로 바뀔 수 있습니다. 기존에는 debug 로그만 남기고 무시했고, 실제로는 copytree가 동일한 이름의 로그 파일을 최종 경로에 복사해주므로 move 실패는 publish 실패와 동등하지 않습니다.

AS-IS: 로그 파일 move 실패 시 logger.debug로만 기록하고 무시했으며, 결과 파일은 copytree로 복사되어 정상 완료로 처리되었습니다.

TO-BE: move를 시도하기 전에 로그 FileHandler를 close하도록 핸들러 정리 로직을 move 앞으로 이동하거나, move 실패를 publish 실패로 승격하지 않고 copytree에 의한 복사로 대체되도록 완화하세요.

final_output_path, f'{_LOG_FILE_PREFIX}{file_time}.txt'))
else:
if log:
log.debug("Moving binary analysis log file is skipped")
except Exception as ex:
publish_ok = False
if log:
log.error(f"Failed to move log file: {ex}")

try:
shutil.copytree(temp_output_path, final_output_path, dirs_exist_ok=True)
except Exception as ex:
publish_ok = False
if log:
log.error(f"Failed to publish scan artifacts: {ex}")

return publish_ok


def init(path_to_find_bin, output_file_name, formats, path_to_exclude=[], temp_path_holder=None):
global logger, _result_log

_json_ext = ".json"
Expand All @@ -85,7 +148,9 @@ def init(path_to_find_bin, output_file_name, formats, path_to_exclude=[]):
output_path = os.path.abspath(output_path)

original_output_path = output_path
output_path = os.path.join(output_path, '.fosslight_temp')
output_path = _prepare_temp_dir(output_path, file_time)
if temp_path_holder is not None:
temp_path_holder["path"] = output_path

while len(output_files) < len(output_extensions):
output_files.append(None)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -125,7 +190,7 @@ def init(path_to_find_bin, output_file_name, formats, path_to_exclude=[]):
logger.error(f"Format error - {msg}")
sys.exit(1)

log_file = os.path.join(output_path, f"fosslight_log_bin_{file_time}.txt")
log_file = os.path.join(output_path, f"{_LOG_FILE_PREFIX}{file_time}.txt")
logger, _result_log = init_log(log_file, True, logging.INFO, logging.DEBUG,
PKG_NAME, path_to_find_bin, path_to_exclude)

Expand Down Expand Up @@ -172,6 +237,23 @@ def get_file_list(path_to_find, excluded_files):
def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="", simple_mode=False,
correct_mode=True, correct_filepath="", path_to_exclude=[],
all_exclude_mode=()):
"""Analyze binaries, leaving no temp directory behind however the run ends.

Ctrl+C (KeyboardInterrupt), an unexpected exception and the sys.exit raised
by error_occured all pass through the finally block.
"""
temp_path_holder = {}
try:
return _analyze_binaries(path_to_find_bin, output_dir, formats, kb_url, kb_token,
simple_mode, correct_mode, correct_filepath, path_to_exclude,
all_exclude_mode, temp_path_holder=temp_path_holder)
finally:
_cleanup_temp_dir(temp_path_holder.get("path"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def _analyze_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="", simple_mode=False,
correct_mode=True, correct_filepath="", path_to_exclude=[],
all_exclude_mode=(), temp_path_holder=None):
global start_time, finish_time, _root_path, _result_log

mode = "Normal Mode"
Expand All @@ -186,7 +268,7 @@ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="",
_result_log, binary_yaml_file, compressed_yaml_file = init_simple(output_dir, PKG_NAME, start_time)
else:
_result_log, result_reports, output_extensions, formats, output_path, original_output_path, log_file = init(
path_to_find_bin, output_dir, formats, path_to_exclude)
path_to_find_bin, output_dir, formats, path_to_exclude, temp_path_holder)

total_bin_cnt = 0
db_loaded_cnt = 0
Expand Down Expand Up @@ -289,23 +371,6 @@ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="",
else:
logger.error(f"Fail to generate result file.:{writing_msg}")

try:
if os.path.isfile(log_file):
move_log_file(log_file, os.path.join(original_output_path, f"fosslight_log_bin_{timestamp_for_filename(start_time)}.txt"))
else:
logger.debug("Moving binary analysis log file is skipped")
except Exception as ex:
logger.debug(f"Failed to move log file: {ex}")

try:
if os.path.isdir(output_path):
shutil.copytree(output_path, original_output_path, dirs_exist_ok=True)
shutil.rmtree(output_path)
else:
logger.debug(f"Temp directory not found, skip moving: {output_path}")
except Exception as ex:
logger.debug(f"Failed to move temp files: {ex}")

try:
print_result_log(mode=mode, success=True, result_log=_result_log,
file_cnt=str(cnt_file_except_skipped),
Expand All @@ -314,6 +379,13 @@ def find_binaries(path_to_find_bin, output_dir, formats, kb_url="", kb_token="",
except Exception as ex:
error_occured(error_msg=f"Print log : {ex}", exit=False)

if not simple_mode:
publish_ok = _finalize_temp_output(
output_path, original_output_path, log_file,
timestamp_for_filename(start_time), logger)
if not publish_ok:
success_to_write = False

return success_to_write, scan_item


Expand Down
Loading
Loading