cisco-ai-skill-scanner 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cisco_ai_skill_scanner-1.0.0.dist-info/METADATA +253 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/RECORD +100 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/WHEEL +4 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/entry_points.txt +4 -0
- cisco_ai_skill_scanner-1.0.0.dist-info/licenses/LICENSE +17 -0
- skillanalyzer/__init__.py +45 -0
- skillanalyzer/_version.py +34 -0
- skillanalyzer/api/__init__.py +25 -0
- skillanalyzer/api/api.py +34 -0
- skillanalyzer/api/api_cli.py +78 -0
- skillanalyzer/api/api_server.py +634 -0
- skillanalyzer/api/router.py +527 -0
- skillanalyzer/cli/__init__.py +25 -0
- skillanalyzer/cli/cli.py +816 -0
- skillanalyzer/config/__init__.py +26 -0
- skillanalyzer/config/config.py +149 -0
- skillanalyzer/config/config_parser.py +122 -0
- skillanalyzer/config/constants.py +85 -0
- skillanalyzer/core/__init__.py +24 -0
- skillanalyzer/core/analyzers/__init__.py +75 -0
- skillanalyzer/core/analyzers/aidefense_analyzer.py +872 -0
- skillanalyzer/core/analyzers/base.py +53 -0
- skillanalyzer/core/analyzers/behavioral/__init__.py +30 -0
- skillanalyzer/core/analyzers/behavioral/alignment/__init__.py +45 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_llm_client.py +240 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_orchestrator.py +216 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_prompt_builder.py +422 -0
- skillanalyzer/core/analyzers/behavioral/alignment/alignment_response_validator.py +136 -0
- skillanalyzer/core/analyzers/behavioral/alignment/threat_vulnerability_classifier.py +198 -0
- skillanalyzer/core/analyzers/behavioral_analyzer.py +453 -0
- skillanalyzer/core/analyzers/cross_skill_analyzer.py +490 -0
- skillanalyzer/core/analyzers/llm_analyzer.py +440 -0
- skillanalyzer/core/analyzers/llm_prompt_builder.py +270 -0
- skillanalyzer/core/analyzers/llm_provider_config.py +215 -0
- skillanalyzer/core/analyzers/llm_request_handler.py +284 -0
- skillanalyzer/core/analyzers/llm_response_parser.py +81 -0
- skillanalyzer/core/analyzers/meta_analyzer.py +845 -0
- skillanalyzer/core/analyzers/static.py +1105 -0
- skillanalyzer/core/analyzers/trigger_analyzer.py +341 -0
- skillanalyzer/core/analyzers/virustotal_analyzer.py +463 -0
- skillanalyzer/core/exceptions.py +77 -0
- skillanalyzer/core/loader.py +377 -0
- skillanalyzer/core/models.py +300 -0
- skillanalyzer/core/reporters/__init__.py +26 -0
- skillanalyzer/core/reporters/json_reporter.py +65 -0
- skillanalyzer/core/reporters/markdown_reporter.py +209 -0
- skillanalyzer/core/reporters/sarif_reporter.py +246 -0
- skillanalyzer/core/reporters/table_reporter.py +195 -0
- skillanalyzer/core/rules/__init__.py +19 -0
- skillanalyzer/core/rules/patterns.py +165 -0
- skillanalyzer/core/rules/yara_scanner.py +157 -0
- skillanalyzer/core/scanner.py +437 -0
- skillanalyzer/core/static_analysis/__init__.py +27 -0
- skillanalyzer/core/static_analysis/cfg/__init__.py +21 -0
- skillanalyzer/core/static_analysis/cfg/builder.py +439 -0
- skillanalyzer/core/static_analysis/context_extractor.py +742 -0
- skillanalyzer/core/static_analysis/dataflow/__init__.py +25 -0
- skillanalyzer/core/static_analysis/dataflow/forward_analysis.py +715 -0
- skillanalyzer/core/static_analysis/interprocedural/__init__.py +21 -0
- skillanalyzer/core/static_analysis/interprocedural/call_graph_analyzer.py +406 -0
- skillanalyzer/core/static_analysis/interprocedural/cross_file_analyzer.py +190 -0
- skillanalyzer/core/static_analysis/parser/__init__.py +21 -0
- skillanalyzer/core/static_analysis/parser/python_parser.py +380 -0
- skillanalyzer/core/static_analysis/semantic/__init__.py +28 -0
- skillanalyzer/core/static_analysis/semantic/name_resolver.py +206 -0
- skillanalyzer/core/static_analysis/semantic/type_analyzer.py +200 -0
- skillanalyzer/core/static_analysis/taint/__init__.py +21 -0
- skillanalyzer/core/static_analysis/taint/tracker.py +252 -0
- skillanalyzer/core/static_analysis/types/__init__.py +36 -0
- skillanalyzer/data/__init__.py +30 -0
- skillanalyzer/data/prompts/boilerplate_protection_rule_prompt.md +26 -0
- skillanalyzer/data/prompts/code_alignment_threat_analysis_prompt.md +901 -0
- skillanalyzer/data/prompts/llm_response_schema.json +71 -0
- skillanalyzer/data/prompts/skill_meta_analysis_prompt.md +303 -0
- skillanalyzer/data/prompts/skill_threat_analysis_prompt.md +263 -0
- skillanalyzer/data/prompts/unified_response_schema.md +97 -0
- skillanalyzer/data/rules/signatures.yaml +440 -0
- skillanalyzer/data/yara_rules/autonomy_abuse.yara +66 -0
- skillanalyzer/data/yara_rules/code_execution.yara +61 -0
- skillanalyzer/data/yara_rules/coercive_injection.yara +115 -0
- skillanalyzer/data/yara_rules/command_injection.yara +54 -0
- skillanalyzer/data/yara_rules/credential_harvesting.yara +115 -0
- skillanalyzer/data/yara_rules/prompt_injection.yara +71 -0
- skillanalyzer/data/yara_rules/script_injection.yara +83 -0
- skillanalyzer/data/yara_rules/skill_discovery_abuse.yara +57 -0
- skillanalyzer/data/yara_rules/sql_injection.yara +73 -0
- skillanalyzer/data/yara_rules/system_manipulation.yara +65 -0
- skillanalyzer/data/yara_rules/tool_chaining_abuse.yara +60 -0
- skillanalyzer/data/yara_rules/transitive_trust_abuse.yara +73 -0
- skillanalyzer/data/yara_rules/unicode_steganography.yara +65 -0
- skillanalyzer/hooks/__init__.py +21 -0
- skillanalyzer/hooks/pre_commit.py +450 -0
- skillanalyzer/threats/__init__.py +25 -0
- skillanalyzer/threats/threats.py +480 -0
- skillanalyzer/utils/__init__.py +28 -0
- skillanalyzer/utils/command_utils.py +129 -0
- skillanalyzer/utils/di_container.py +154 -0
- skillanalyzer/utils/file_utils.py +86 -0
- skillanalyzer/utils/logging_config.py +96 -0
- skillanalyzer/utils/logging_utils.py +71 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Dependency Injection Container for Skill Analyzer.
|
|
19
|
+
|
|
20
|
+
This module provides a simple dependency injection container to improve
|
|
21
|
+
testability and decouple configuration from implementation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from typing import Any, TypeVar
|
|
26
|
+
|
|
27
|
+
from ..config.config import Config
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DIContainer:
|
|
33
|
+
"""Simple dependency injection container."""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""Initialize the container."""
|
|
37
|
+
self._services: dict[type, Any] = {}
|
|
38
|
+
self._singletons: dict[type, Any] = {}
|
|
39
|
+
|
|
40
|
+
def register(self, service_type: type[T], instance: T, singleton: bool = True) -> None:
|
|
41
|
+
"""Register a service instance.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
service_type: The type/interface to register.
|
|
45
|
+
instance: The instance to register.
|
|
46
|
+
singleton: Whether to treat as singleton (default: True).
|
|
47
|
+
"""
|
|
48
|
+
if singleton:
|
|
49
|
+
self._singletons[service_type] = instance
|
|
50
|
+
else:
|
|
51
|
+
self._services[service_type] = instance
|
|
52
|
+
|
|
53
|
+
def register_factory(self, service_type: type[T], factory: Callable[[], T], singleton: bool = True) -> None:
|
|
54
|
+
"""Register a factory function for creating instances.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
service_type: The type/interface to register.
|
|
58
|
+
factory: Factory function that creates instances.
|
|
59
|
+
singleton: Whether to treat as singleton (default: True).
|
|
60
|
+
"""
|
|
61
|
+
if singleton:
|
|
62
|
+
self._singletons[service_type] = factory()
|
|
63
|
+
else:
|
|
64
|
+
self._services[service_type] = factory
|
|
65
|
+
|
|
66
|
+
def get(self, service_type: type[T]) -> T | None:
|
|
67
|
+
"""Get a service instance.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
service_type: The type to retrieve.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The service instance or None if not found.
|
|
74
|
+
"""
|
|
75
|
+
if service_type in self._singletons:
|
|
76
|
+
return self._singletons[service_type]
|
|
77
|
+
|
|
78
|
+
if service_type in self._services:
|
|
79
|
+
service = self._services[service_type]
|
|
80
|
+
if callable(service):
|
|
81
|
+
return service()
|
|
82
|
+
return service
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def get_or_create(self, service_type: type[T], factory: Callable[[], T] | None = None) -> T:
|
|
87
|
+
"""Get a service instance or create with factory if not found.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
service_type: The type to retrieve.
|
|
91
|
+
factory: Optional factory function if service not registered.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The service instance.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If service not found and no factory provided.
|
|
98
|
+
"""
|
|
99
|
+
instance = self.get(service_type)
|
|
100
|
+
if instance is not None:
|
|
101
|
+
return instance
|
|
102
|
+
|
|
103
|
+
if factory is not None:
|
|
104
|
+
instance = factory()
|
|
105
|
+
self.register(service_type, instance)
|
|
106
|
+
return instance
|
|
107
|
+
|
|
108
|
+
raise ValueError(f"Service {service_type} not registered and no factory provided")
|
|
109
|
+
|
|
110
|
+
def clear(self) -> None:
|
|
111
|
+
"""Clear all registered services."""
|
|
112
|
+
self._services.clear()
|
|
113
|
+
self._singletons.clear()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Global container instance
|
|
117
|
+
_container = DIContainer()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_container() -> DIContainer:
|
|
121
|
+
"""Get the global DI container instance.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The global container instance.
|
|
125
|
+
"""
|
|
126
|
+
return _container
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def configure_default_services(config: Config | None = None) -> None:
|
|
130
|
+
"""Configure default services in the container.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
config: Optional config instance to register.
|
|
134
|
+
"""
|
|
135
|
+
container = get_container()
|
|
136
|
+
|
|
137
|
+
if config:
|
|
138
|
+
container.register(Config, config)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def inject_config() -> Config:
|
|
142
|
+
"""Inject Config dependency.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Config instance from container.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If Config not registered in container.
|
|
149
|
+
"""
|
|
150
|
+
container = get_container()
|
|
151
|
+
config = container.get(Config)
|
|
152
|
+
if config is None:
|
|
153
|
+
raise ValueError("Config not registered in DI container. Call configure_default_services() first.")
|
|
154
|
+
return config
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
File utility functions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_file_safe(file_path: Path, max_size_mb: int = 10) -> str | None:
|
|
25
|
+
"""
|
|
26
|
+
Safely read a file with size limit.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Path to file
|
|
30
|
+
max_size_mb: Maximum file size in MB
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
File content or None if unreadable
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
size_bytes = file_path.stat().st_size
|
|
37
|
+
max_bytes = max_size_mb * 1024 * 1024
|
|
38
|
+
|
|
39
|
+
if size_bytes > max_bytes:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
with open(file_path, encoding="utf-8") as f:
|
|
43
|
+
return f.read()
|
|
44
|
+
except (OSError, UnicodeDecodeError):
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_file_type(file_path: Path) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Determine file type from extension.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
file_path: Path to file
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
File type string
|
|
57
|
+
"""
|
|
58
|
+
suffix = file_path.suffix.lower()
|
|
59
|
+
|
|
60
|
+
type_mapping = {
|
|
61
|
+
".py": "python",
|
|
62
|
+
".sh": "bash",
|
|
63
|
+
".bash": "bash",
|
|
64
|
+
".md": "markdown",
|
|
65
|
+
".markdown": "markdown",
|
|
66
|
+
".exe": "binary",
|
|
67
|
+
".so": "binary",
|
|
68
|
+
".dylib": "binary",
|
|
69
|
+
".dll": "binary",
|
|
70
|
+
".bin": "binary",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return type_mapping.get(suffix, "other")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_binary_file(file_path: Path) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Check if file is binary.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
file_path: Path to file
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if binary
|
|
85
|
+
"""
|
|
86
|
+
return get_file_type(file_path) == "binary"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Centralized logging configuration for Skill Analyzer.
|
|
19
|
+
|
|
20
|
+
This module provides consistent logging setup across all components.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def setup_logger(name: str, level: str | None = None, format_string: str | None = None) -> logging.Logger:
|
|
28
|
+
"""
|
|
29
|
+
Set up a logger with consistent configuration.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Logger name (typically __name__)
|
|
33
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
34
|
+
format_string: Custom format string, uses default if None
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Configured logger instance
|
|
38
|
+
"""
|
|
39
|
+
logger = logging.getLogger(name)
|
|
40
|
+
|
|
41
|
+
if logger.handlers:
|
|
42
|
+
return logger
|
|
43
|
+
|
|
44
|
+
skillanalyzer_root = logging.getLogger("skillanalyzer")
|
|
45
|
+
if skillanalyzer_root.level == logging.DEBUG and name.startswith("skillanalyzer"):
|
|
46
|
+
logger.setLevel(logging.DEBUG)
|
|
47
|
+
elif level:
|
|
48
|
+
logger.setLevel(getattr(logging, level.upper()))
|
|
49
|
+
else:
|
|
50
|
+
logger.setLevel(logging.INFO)
|
|
51
|
+
|
|
52
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
53
|
+
handler.setLevel(logger.level)
|
|
54
|
+
|
|
55
|
+
default_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
56
|
+
formatter = logging.Formatter(format_string or default_format)
|
|
57
|
+
handler.setFormatter(formatter)
|
|
58
|
+
|
|
59
|
+
logger.addHandler(handler)
|
|
60
|
+
logger.propagate = False
|
|
61
|
+
|
|
62
|
+
return logger
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_logger(name: str, level: str | None = None) -> logging.Logger:
|
|
66
|
+
"""
|
|
67
|
+
Get a logger with standard configuration.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
name: Logger name (typically __name__)
|
|
71
|
+
level: Optional logging level override
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Configured logger instance
|
|
75
|
+
"""
|
|
76
|
+
return setup_logger(name, level)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def set_verbose_logging(verbose: bool = False) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Enable or disable verbose logging for all skillanalyzer loggers.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
verbose: If True, set all existing skillanalyzer loggers to DEBUG level
|
|
85
|
+
"""
|
|
86
|
+
target_level = logging.DEBUG if verbose else logging.INFO
|
|
87
|
+
|
|
88
|
+
root_logger = logging.getLogger("skillanalyzer")
|
|
89
|
+
root_logger.setLevel(target_level)
|
|
90
|
+
|
|
91
|
+
for name in list(logging.Logger.manager.loggerDict.keys()):
|
|
92
|
+
if name.startswith("skillanalyzer"):
|
|
93
|
+
logger = logging.getLogger(name)
|
|
94
|
+
logger.setLevel(target_level)
|
|
95
|
+
for handler in logger.handlers:
|
|
96
|
+
handler.setLevel(target_level)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Copyright 2026 Cisco Systems, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Logging utility functions.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def setup_logger(name: str, level: int = logging.INFO, log_file: str | None = None) -> logging.Logger:
|
|
26
|
+
"""
|
|
27
|
+
Set up a logger with console and optional file output.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: Logger name
|
|
31
|
+
level: Logging level
|
|
32
|
+
log_file: Optional file path for logs
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Configured logger
|
|
36
|
+
"""
|
|
37
|
+
logger = logging.getLogger(name)
|
|
38
|
+
logger.setLevel(level)
|
|
39
|
+
|
|
40
|
+
# Remove existing handlers
|
|
41
|
+
logger.handlers = []
|
|
42
|
+
|
|
43
|
+
# Console handler
|
|
44
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
45
|
+
console_handler.setLevel(level)
|
|
46
|
+
|
|
47
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
48
|
+
console_handler.setFormatter(formatter)
|
|
49
|
+
logger.addHandler(console_handler)
|
|
50
|
+
|
|
51
|
+
# File handler if specified
|
|
52
|
+
if log_file:
|
|
53
|
+
file_handler = logging.FileHandler(log_file)
|
|
54
|
+
file_handler.setLevel(level)
|
|
55
|
+
file_handler.setFormatter(formatter)
|
|
56
|
+
logger.addHandler(file_handler)
|
|
57
|
+
|
|
58
|
+
return logger
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_logger(name: str) -> logging.Logger:
|
|
62
|
+
"""
|
|
63
|
+
Get or create a logger.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Logger name
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Logger instance
|
|
70
|
+
"""
|
|
71
|
+
return logging.getLogger(name)
|