codinsight 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (25) hide show
  1. {codinsight-0.2.0 → codinsight-0.3.0}/PKG-INFO +1 -1
  2. {codinsight-0.2.0 → codinsight-0.3.0}/pyproject.toml +17 -1
  3. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/redundancy.py +1 -2
  4. codinsight-0.3.0/src/code_insight/code_analysis/security.py +327 -0
  5. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/core.py +14 -4
  6. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/multi_analysis.py +9 -10
  7. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/trend_analysis/trend_analysis.py +1 -1
  8. {codinsight-0.2.0 → codinsight-0.3.0}/src/codinsight.egg-info/PKG-INFO +1 -1
  9. {codinsight-0.2.0 → codinsight-0.3.0}/src/codinsight.egg-info/SOURCES.txt +1 -0
  10. {codinsight-0.2.0 → codinsight-0.3.0}/README.md +0 -0
  11. {codinsight-0.2.0 → codinsight-0.3.0}/setup.cfg +0 -0
  12. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/__init__.py +0 -0
  13. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/__init__.py +0 -0
  14. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/abstract.py +0 -0
  15. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/algorithm.py +0 -0
  16. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/complexity.py +0 -0
  17. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/quality.py +0 -0
  18. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/readability.py +0 -0
  19. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/struct.py +0 -0
  20. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/code_analysis/style.py +0 -0
  21. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/py.typed +0 -0
  22. {codinsight-0.2.0 → codinsight-0.3.0}/src/code_insight/trend_analysis/__init__.py +0 -0
  23. {codinsight-0.2.0 → codinsight-0.3.0}/src/codinsight.egg-info/dependency_links.txt +0 -0
  24. {codinsight-0.2.0 → codinsight-0.3.0}/src/codinsight.egg-info/requires.txt +0 -0
  25. {codinsight-0.2.0 → codinsight-0.3.0}/src/codinsight.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codinsight
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codinsight"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Add your description here"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -35,6 +35,22 @@ addopts = "--exclude=tests"
35
35
  packages = ["src/code_insight"]
36
36
  include = ["src/code_insight/py.typed"]
37
37
 
38
+ [tool.ruff]
39
+ line-length = 88
40
+ include = ["codinsight/**/*.py"]
41
+
42
+ [tool.ruff.lint]
43
+ select = ["F", "E", "I", "B"]
44
+
45
+ [tool.ruff.lint.isort]
46
+ # black profileと同等の設定
47
+ split-on-trailing-comma = true
48
+ combine-as-imports = true
49
+
50
+ [tool.ruff.format]
51
+ quote-style = "double"
52
+ indent-style = "space"
53
+
38
54
  [tool.pytest.ini_options]
39
55
  testpaths = ["tests"]
40
56
  pythonpath = ["src"]
@@ -286,8 +286,7 @@ class Redundancy(AbstractAnalysis[RedundancyAnalysisResult, RedundancyAnalysisCo
286
286
  if isinstance(node_value, bytes):
287
287
  node_value = node_value.decode()
288
288
  structure_elements.append(
289
- f"return_const_{type(node.value.value).__name__}_"
290
- f"{node_value}"
289
+ f"return_const_{type(node.value.value).__name__}_{node_value}"
291
290
  )
292
291
  elif isinstance(node.value, ast.BinOp):
293
292
  structure_elements.append(
@@ -0,0 +1,327 @@
1
+ import ast
2
+ import re
3
+ from typing import List, Set
4
+
5
+ from code_insight.code_analysis.abstract import (
6
+ AbstractAnalysis,
7
+ BaseAnalysisConfig,
8
+ BaseAnalysisResult,
9
+ )
10
+
11
+
12
+ class SecurityAnalysisConfig(BaseAnalysisConfig):
13
+ """
14
+ セキュリティ解析設定
15
+
16
+ Attributes
17
+ ----------
18
+ check_hardcoded_secrets : bool
19
+ ハードコードされた秘密情報をチェックするか, by default True
20
+ check_dangerous_functions : bool
21
+ 危険な関数の使用をチェックするか, by default True
22
+ check_sql_injection : bool
23
+ SQLインジェクション脆弱性をチェックするか, by default True
24
+ secret_patterns : List[str]
25
+ 秘密情報検出パターン, by default 標準パターン
26
+ """
27
+
28
+ check_hardcoded_secrets: bool = True
29
+ check_dangerous_functions: bool = True
30
+ check_sql_injection: bool = True
31
+ secret_patterns: List[str] = [
32
+ r"password\s*=\s*['\"][^'\"]{3,}['\"]",
33
+ r"api_key\s*=\s*['\"][^'\"]{10,}['\"]",
34
+ r"secret\s*=\s*['\"][^'\"]{8,}['\"]",
35
+ r"token\s*=\s*['\"][^'\"]{10,}['\"]",
36
+ r"key\s*=\s*['\"][^'\"]{8,}['\"]",
37
+ ]
38
+
39
+
40
+ class SecurityAnalysisResult(BaseAnalysisResult):
41
+ """
42
+ 解析結果(セキュリティ)
43
+
44
+ Attributes
45
+ ----------
46
+ hardcoded_secrets_count : int
47
+ ハードコードされた秘密情報の数
48
+ dangerous_function_count : int
49
+ 危険な関数の使用数
50
+ sql_injection_risk_count : int
51
+ SQLインジェクション脆弱性の可能性がある箇所の数
52
+ input_validation_missing_count : int
53
+ 入力検証不備の数
54
+ security_score : float
55
+ セキュリティスコア(0.0-1.0、高いほど安全)
56
+ """
57
+
58
+ hardcoded_secrets_count: int
59
+ dangerous_function_count: int
60
+ sql_injection_risk_count: int
61
+ input_validation_missing_count: int
62
+ security_score: float
63
+
64
+
65
+ class Security(AbstractAnalysis[SecurityAnalysisResult, SecurityAnalysisConfig]):
66
+ """
67
+ 解析クラス(セキュリティ)
68
+
69
+ Notes
70
+ -----
71
+ コードのセキュリティ脆弱性を多角的に解析するクラス
72
+ """
73
+
74
+ def __init__(self, config: SecurityAnalysisConfig | None = None) -> None:
75
+ """
76
+ コンストラクタ
77
+
78
+ Parameters
79
+ ----------
80
+ config : SecurityAnalysisConfig | None, optional
81
+ セキュリティ解析設定, by default None
82
+ """
83
+ super().__init__(config)
84
+
85
+ def get_default_config(self) -> SecurityAnalysisConfig:
86
+ """
87
+ デフォルト設定を取得
88
+
89
+ Returns
90
+ -------
91
+ SecurityAnalysisConfig
92
+ デフォルトのセキュリティ解析設定
93
+ """
94
+ return SecurityAnalysisConfig()
95
+
96
+ def analyze(self, source_code: str) -> SecurityAnalysisResult:
97
+ """
98
+ コード解析
99
+
100
+ Parameters
101
+ ----------
102
+ source_code : str
103
+ 解析対象のソースコード
104
+
105
+ Returns
106
+ -------
107
+ SecurityAnalysisResult
108
+ セキュリティ解析結果
109
+ """
110
+ if not self.config.enabled:
111
+ return SecurityAnalysisResult(
112
+ hardcoded_secrets_count=0,
113
+ dangerous_function_count=0,
114
+ sql_injection_risk_count=0,
115
+ input_validation_missing_count=0,
116
+ security_score=1.0,
117
+ )
118
+
119
+ tree = self.parse_source_code(source_code)
120
+
121
+ hardcoded_secrets = self.get_hardcoded_secrets_count(source_code, tree)
122
+ dangerous_functions = self.get_dangerous_function_count(source_code, tree)
123
+ sql_injection_risks = self.get_sql_injection_risk_count(source_code, tree)
124
+ input_validation_missing = self.get_input_validation_missing_count(source_code, tree)
125
+
126
+ security_score = self.calculate_security_score(
127
+ hardcoded_secrets, dangerous_functions, sql_injection_risks, input_validation_missing
128
+ )
129
+
130
+ return SecurityAnalysisResult(
131
+ hardcoded_secrets_count=hardcoded_secrets,
132
+ dangerous_function_count=dangerous_functions,
133
+ sql_injection_risk_count=sql_injection_risks,
134
+ input_validation_missing_count=input_validation_missing,
135
+ security_score=security_score,
136
+ )
137
+
138
+ def parse_source_code(self, source_code: str) -> ast.AST:
139
+ """
140
+ ソースコードを解析
141
+
142
+ Parameters
143
+ ----------
144
+ source_code : str
145
+ 解析対象のソースコード
146
+
147
+ Returns
148
+ -------
149
+ ast.AST
150
+ 解析済みのAST
151
+ """
152
+ return ast.parse(source_code)
153
+
154
+ def get_hardcoded_secrets_count(
155
+ self, source_code: str, tree: ast.AST | None = None
156
+ ) -> int:
157
+ """
158
+ ハードコードされた秘密情報の数を取得
159
+
160
+ Parameters
161
+ ----------
162
+ source_code : str
163
+ 解析対象のソースコード
164
+ tree : ast.AST | None, optional
165
+ 解析済みのAST, by default None
166
+
167
+ Returns
168
+ -------
169
+ int
170
+ ハードコードされた秘密情報の数
171
+ """
172
+ if not self.config.check_hardcoded_secrets:
173
+ return 0
174
+
175
+ count = 0
176
+ source_lower = source_code.lower()
177
+
178
+ for pattern in self.config.secret_patterns:
179
+ matches = re.findall(pattern, source_lower, re.IGNORECASE)
180
+ count += len(matches)
181
+
182
+ return count
183
+
184
+ def get_dangerous_function_count(
185
+ self, source_code: str, tree: ast.AST | None = None
186
+ ) -> int:
187
+ """
188
+ 危険な関数の使用数を取得
189
+
190
+ Parameters
191
+ ----------
192
+ source_code : str
193
+ 解析対象のソースコード
194
+ tree : ast.AST | None, optional
195
+ 解析済みのAST, by default None
196
+
197
+ Returns
198
+ -------
199
+ int
200
+ 危険な関数の使用数
201
+ """
202
+ if not self.config.check_dangerous_functions:
203
+ return 0
204
+
205
+ tree = tree or self.parse_source_code(source_code)
206
+ dangerous_functions = {"eval", "exec", "compile", "__import__"}
207
+ count = 0
208
+
209
+ for node in ast.walk(tree):
210
+ if isinstance(node, ast.Call):
211
+ if isinstance(node.func, ast.Name) and node.func.id in dangerous_functions:
212
+ count += 1
213
+
214
+ return count
215
+
216
+ def get_sql_injection_risk_count(
217
+ self, source_code: str, tree: ast.AST | None = None
218
+ ) -> int:
219
+ """
220
+ SQLインジェクション脆弱性の可能性がある箇所の数を取得
221
+
222
+ Parameters
223
+ ----------
224
+ source_code : str
225
+ 解析対象のソースコード
226
+ tree : ast.AST | None, optional
227
+ 解析済みのAST, by default None
228
+
229
+ Returns
230
+ -------
231
+ int
232
+ SQLインジェクション脆弱性の可能性がある箇所の数
233
+ """
234
+ if not self.config.check_sql_injection:
235
+ return 0
236
+
237
+ tree = tree or self.parse_source_code(source_code)
238
+ count = 0
239
+
240
+ sql_patterns = [
241
+ r"select\s+.*\s+from\s+.*\+",
242
+ r"insert\s+into\s+.*\+",
243
+ r"update\s+.*\s+set\s+.*\+",
244
+ r"delete\s+from\s+.*\+",
245
+ ]
246
+
247
+ for node in ast.walk(tree):
248
+ if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
249
+ if isinstance(node.left, ast.Constant) and isinstance(node.left.value, str):
250
+ sql_text = node.left.value.lower()
251
+ for pattern in sql_patterns:
252
+ if re.search(pattern, sql_text):
253
+ count += 1
254
+ break
255
+
256
+ return count
257
+
258
+ def get_input_validation_missing_count(
259
+ self, source_code: str, tree: ast.AST | None = None
260
+ ) -> int:
261
+ """
262
+ 入力検証不備の数を取得
263
+
264
+ Parameters
265
+ ----------
266
+ source_code : str
267
+ 解析対象のソースコード
268
+ tree : ast.AST | None, optional
269
+ 解析済みのAST, by default None
270
+
271
+ Returns
272
+ -------
273
+ int
274
+ 入力検証不備の数
275
+ """
276
+ tree = tree or self.parse_source_code(source_code)
277
+ count = 0
278
+ input_functions = {"input", "raw_input"}
279
+
280
+ for node in ast.walk(tree):
281
+ if isinstance(node, ast.Call):
282
+ if isinstance(node.func, ast.Name) and node.func.id in input_functions:
283
+ parent_found = False
284
+ for parent in ast.walk(tree):
285
+ if isinstance(parent, ast.If):
286
+ for child in ast.walk(parent):
287
+ if child is node:
288
+ parent_found = True
289
+ break
290
+ if not parent_found:
291
+ count += 1
292
+
293
+ return count
294
+
295
+ def calculate_security_score(
296
+ self,
297
+ hardcoded_secrets: int,
298
+ dangerous_functions: int,
299
+ sql_injection_risks: int,
300
+ input_validation_missing: int,
301
+ ) -> float:
302
+ """
303
+ セキュリティスコアを計算
304
+
305
+ Parameters
306
+ ----------
307
+ hardcoded_secrets : int
308
+ ハードコードされた秘密情報の数
309
+ dangerous_functions : int
310
+ 危険な関数の使用数
311
+ sql_injection_risks : int
312
+ SQLインジェクション脆弱性の可能性がある箇所の数
313
+ input_validation_missing : int
314
+ 入力検証不備の数
315
+
316
+ Returns
317
+ -------
318
+ float
319
+ セキュリティスコア(0.0-1.0、高いほど安全)
320
+ """
321
+ total_issues = hardcoded_secrets + dangerous_functions + sql_injection_risks + input_validation_missing
322
+
323
+ if total_issues == 0:
324
+ return 1.0
325
+
326
+ penalty = min(total_issues * 0.1, 1.0)
327
+ return max(1.0 - penalty, 0.0)
@@ -1,5 +1,5 @@
1
1
  from enum import StrEnum, auto
2
- from typing import Any, Type
2
+ from typing import Any
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
@@ -16,6 +16,7 @@ from code_insight.code_analysis.readability import (
16
16
  ReadabilityAnalysisConfig,
17
17
  )
18
18
  from code_insight.code_analysis.redundancy import Redundancy, RedundancyAnalysisConfig
19
+ from code_insight.code_analysis.security import Security, SecurityAnalysisConfig
19
20
  from code_insight.code_analysis.struct import Struct, StructAnalysisConfig
20
21
  from code_insight.code_analysis.style import Style, StyleAnalysisConfig
21
22
 
@@ -40,6 +41,8 @@ class AnalysisConfigs(BaseModel):
40
41
  複雑度解析設定, by default None
41
42
  quality : QualityAnalysisConfig | None
42
43
  品質解析設定, by default None
44
+ security : SecurityAnalysisConfig | None
45
+ セキュリティ解析設定, by default None
43
46
  """
44
47
 
45
48
  style: StyleAnalysisConfig | None = None
@@ -49,6 +52,7 @@ class AnalysisConfigs(BaseModel):
49
52
  algorithm: AlgorithmAnalysisConfig | None = None
50
53
  complexity: ComplexityAnalysisConfig | None = None
51
54
  quality: QualityAnalysisConfig | None = None
55
+ security: SecurityAnalysisConfig | None = None
52
56
 
53
57
 
54
58
  class CodeAnalysisType(StrEnum):
@@ -71,6 +75,8 @@ class CodeAnalysisType(StrEnum):
71
75
  複雑度解析
72
76
  QUALITY : str
73
77
  品質解析
78
+ SECURITY : str
79
+ セキュリティ解析
74
80
  """
75
81
 
76
82
  STYLE = auto()
@@ -80,6 +86,7 @@ class CodeAnalysisType(StrEnum):
80
86
  ALGORITHM = auto()
81
87
  COMPLEXITY = auto()
82
88
  QUALITY = auto()
89
+ SECURITY = auto()
83
90
 
84
91
  @staticmethod
85
92
  def get_code_analysis_class(
@@ -119,6 +126,8 @@ class CodeAnalysisType(StrEnum):
119
126
  return Complexity(config) # type: ignore
120
127
  elif type == CodeAnalysisType.QUALITY:
121
128
  return Quality(config) # type: ignore
129
+ elif type == CodeAnalysisType.SECURITY:
130
+ return Security(config) # type: ignore
122
131
  else:
123
132
  raise ValueError(f"Invalid code analysis type: {type}")
124
133
 
@@ -156,7 +165,7 @@ class CodeAnalysis:
156
165
 
157
166
  def analyze(
158
167
  self, types: list[CodeAnalysisType]
159
- ) -> dict[CodeAnalysisType, Type[BaseAnalysisResult]]:
168
+ ) -> dict[CodeAnalysisType, BaseAnalysisResult]:
160
169
  """
161
170
  コード解析
162
171
 
@@ -167,10 +176,10 @@ class CodeAnalysis:
167
176
 
168
177
  Returns
169
178
  -------
170
- dict[CodeAnalysisType, Type[BaseAnalysisResult]]
179
+ dict[CodeAnalysisType, BaseAnalysisResult]
171
180
  解析結果の辞書
172
181
  """
173
- result: dict[CodeAnalysisType, Type[BaseAnalysisResult]] = {}
182
+ result: dict[CodeAnalysisType, BaseAnalysisResult] = {}
174
183
  for type in types:
175
184
  config = self._get_config_for_type(type)
176
185
  result[type] = CodeAnalysisType.get_code_analysis_class(
@@ -205,5 +214,6 @@ class CodeAnalysis:
205
214
  CodeAnalysisType.ALGORITHM: self.configs.algorithm,
206
215
  CodeAnalysisType.COMPLEXITY: self.configs.complexity,
207
216
  CodeAnalysisType.QUALITY: self.configs.quality,
217
+ CodeAnalysisType.SECURITY: self.configs.security,
208
218
  }
209
219
  return config_map.get(analysis_type)
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  from pathlib import Path
3
- from typing import Any, Iterable, cast
3
+ from typing import Iterable
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
@@ -24,7 +24,7 @@ class FileAnalysisResult(BaseModel):
24
24
  """
25
25
 
26
26
  path: str
27
- results: dict[str, dict[str, Any]]
27
+ results: dict[CodeAnalysisType, BaseAnalysisResult]
28
28
 
29
29
 
30
30
  class AggregateStats(BaseModel):
@@ -46,7 +46,7 @@ class AggregateStats(BaseModel):
46
46
  total_files: int
47
47
  analyzed_files: int
48
48
  errors: list[str]
49
- by_type_avg: dict[str, dict[str, float]]
49
+ by_type_avg: dict[CodeAnalysisType, dict[str, float]]
50
50
 
51
51
 
52
52
  class MultiAnalysisResult(BaseModel):
@@ -169,16 +169,15 @@ def analyze_file(
169
169
  source_code = path.read_text(encoding="utf-8", errors="ignore")
170
170
  analysis = CodeAnalysis(source_code=source_code, configs=configs)
171
171
  result_map = analysis.analyze(types)
172
- as_dict: dict[str, dict[str, Any]] = {}
172
+ as_dict: dict[CodeAnalysisType, BaseAnalysisResult] = {}
173
173
  for t, model in result_map.items():
174
- m = cast(BaseAnalysisResult, model)
175
- as_dict[t.name] = m.model_dump()
174
+ as_dict[t] = model
176
175
  return FileAnalysisResult(path=str(path), results=as_dict)
177
176
 
178
177
 
179
178
  def _aggregate_numeric_means(
180
179
  files: list[FileAnalysisResult],
181
- ) -> dict[str, dict[str, float]]:
180
+ ) -> dict[CodeAnalysisType, dict[str, float]]:
182
181
  """
183
182
  数値メトリクスの平均値を集約
184
183
 
@@ -192,17 +191,17 @@ def _aggregate_numeric_means(
192
191
  dict[str, dict[str, float]]
193
192
  解析タイプ別の平均値辞書
194
193
  """
195
- by_type: dict[str, dict[str, list[float]]] = {}
194
+ by_type: dict[CodeAnalysisType, dict[str, list[float]]] = {}
196
195
 
197
196
  for fa in files:
198
197
  for tname, metrics in fa.results.items():
199
198
  if tname not in by_type:
200
199
  by_type[tname] = {}
201
- for key, val in metrics.items():
200
+ for key, val in metrics.model_dump().items():
202
201
  if isinstance(val, (int, float)):
203
202
  by_type[tname].setdefault(key, []).append(float(val))
204
203
 
205
- avg: dict[str, dict[str, float]] = {}
204
+ avg: dict[CodeAnalysisType, dict[str, float]] = {}
206
205
  for tname, metrics_map in by_type.items():
207
206
  avg[tname] = {}
208
207
  for key, values in metrics_map.items():
@@ -167,7 +167,7 @@ class TrendAnalysis:
167
167
  X_vis[i, 0] + 0.02,
168
168
  X_vis[i, 1] + 0.02,
169
169
  self.code_labels[i],
170
- fontsize=8,
170
+ fontsize=7,
171
171
  )
172
172
 
173
173
  plt.title("Clustering Result")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codinsight
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -11,6 +11,7 @@ src/code_insight/code_analysis/complexity.py
11
11
  src/code_insight/code_analysis/quality.py
12
12
  src/code_insight/code_analysis/readability.py
13
13
  src/code_insight/code_analysis/redundancy.py
14
+ src/code_insight/code_analysis/security.py
14
15
  src/code_insight/code_analysis/struct.py
15
16
  src/code_insight/code_analysis/style.py
16
17
  src/code_insight/trend_analysis/__init__.py
File without changes
File without changes