@shundoo-ai/dsh-cosmic 1.0.21 → 1.0.22
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.
- package/package.json +1 -1
- package/skills/cosmic-build/LOCAL-PATCHES.md +43 -14
- package/skills/cosmic-build/SKILL.md +24 -9
- package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +26 -17
- package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +96 -2
- package/skills/cosmic-build/knowledge/cosmic/arch/security/security_standard.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/allimport.md +2 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/core/dynamic-object-collection.md +6 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/core/model.md +9 -9
- package/skills/cosmic-build/knowledge/cosmic/platform/core/view.md +5 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +8 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +5 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/print-plugin-guide.md +1 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/report-plugin.md +269 -57
- package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +130 -23
- package/skills/cosmic-build/knowledge/cosmic/platform/reporttreedata-plugin.md +48 -28
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/_index.md +12 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/attachment-name-process.md +17 -13
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/dynamic-update-field.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/filter-grid/filter-grid-set-value.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-basedatafield.md +2 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-container-init.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-popup.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-value-parse.md +5 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/list-show-form.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/query-field-dynamic.md +0 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/business-data/delete-service.md +24 -29
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-batch.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-direct-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/entity-object-access.md +8 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/qfilter-compose.md +6 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/after-bind-data-set-visible.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/click-return-data.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/closed-call-back-set-value.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/register-listener.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/set-filter-tab-org.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dispatch-service/microservice-dev.md +18 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dist-lock/dist-lock-basic.md +3 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/mq/mq-subscribe.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/async-operation.md +10 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/big-task-async.md +4 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/schedule-task.md +9 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-interactive.md +33 -34
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-plugin-events.md +30 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-async-query.md +133 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-batch-query.md +26 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-chart.md +112 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-dataset-assembly.md +200 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-head-filter.md +177 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-hyperlink-drill.md +216 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-list-data-query.md +12 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-merge-column.md +44 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-print-data.md +163 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-row-data-toolbar.md +188 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-subtotal-total.md +139 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-data-query.md +108 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-list.md +181 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/system-service/report-plugin-interface.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-basedata-cascade-confirm.md +14 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-before-item-validate.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-build-filter.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-multi-level-filter.md +11 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-refresh-node.md +22 -15
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-entity-model.md +3 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-exception.md +9 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-orm-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-request-context.md +7 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/standards.md +28 -3
- package/skills/cosmic-build/references/compile-guide.md +36 -1
- package/skills/cosmic-build/references/dcs-code-rules.md +180 -0
- package/skills/cosmic-build/references/delivery.md +16 -2
- package/skills/cosmic-build/references/execution-contract.md +15 -3
- package/skills/cosmic-build/references/java-plugin-delivery.md +21 -11
- package/skills/cosmic-build/references/plugin-handoff-input.md +82 -24
- package/skills/cosmic-build/references/plugin-runtime-routing.md +23 -6
- package/skills/cosmic-build/references/report-filter-contract.md +53 -15
- package/skills/cosmic-build/references/sdk-inspection.md +4 -1
- package/skills/cosmic-build/references/type-registry.json +26 -17
- package/skills/cosmic-build/schemas/data-model-authoring.schema.json +1 -1
- package/skills/cosmic-build/scripts/_http_client.py +3 -0
- package/skills/cosmic-build/scripts/compile_plugin_source.py +251 -42
- package/skills/cosmic-build/scripts/configure_list.py +112 -0
- package/skills/cosmic-build/scripts/dcs-rule-catalog.json +799 -0
- package/skills/cosmic-build/scripts/dcs_code_gate.py +1976 -0
- package/skills/cosmic-build/scripts/execution_binding.py +25 -0
- package/skills/cosmic-build/scripts/platform_api_contract.py +352 -7
- package/skills/cosmic-build/scripts/plugin_source_gate.py +9 -1
- package/skills/cosmic-build/scripts/plugin_target_sdk.py +12 -8
- package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +488 -88
- package/skills/cosmic-build/scripts/query_plugin_handoff.py +207 -20
- package/skills/cosmic-build/scripts/register_plugins.py +27 -10
- package/skills/cosmic-build/scripts/render_output_cards.py +57 -20
- package/skills/cosmic-build/scripts/resolve_output_path.py +60 -26
- package/skills/cosmic-build/scripts/run_java_tests.py +162 -0
- package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +28 -0
- package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +1 -22
- package/skills/cosmic-build/scripts/test/fixtures/list_column_property_declarations.json +887 -0
- package/skills/cosmic-build/scripts/test/fixtures/operation-entry-field-prepare.java +78 -0
- package/skills/cosmic-build/scripts/test/test_add_field_required_contract.py +12 -0
- package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +23 -2
- package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +287 -4
- package/skills/cosmic-build/scripts/test/test_configure_list_delivery.py +132 -0
- package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +58 -1
- package/skills/cosmic-build/scripts/test/test_data_model_contract.py +19 -0
- package/skills/cosmic-build/scripts/test/test_dcs_code_gate.py +901 -0
- package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +1 -1
- package/skills/cosmic-build/scripts/test/test_direct_cli_architecture.py +2 -0
- package/skills/cosmic-build/scripts/test/test_http_client.py +6 -0
- package/skills/cosmic-build/scripts/test/test_java_toolchain_feedback.py +206 -0
- package/skills/cosmic-build/scripts/test/test_kd_api.py +21 -0
- package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +184 -0
- package/skills/cosmic-build/scripts/test/test_mcp_cli.py +139 -2
- package/skills/cosmic-build/scripts/test/test_menu_creation_closure.py +16 -0
- package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +55 -1
- package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +1 -1
- package/skills/cosmic-build/scripts/test/test_operation_field_preparation.py +84 -0
- package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +322 -1
- package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +951 -4
- package/skills/cosmic-build/scripts/test/test_plugin_source_gate.py +30 -2
- package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +21 -0
- package/skills/cosmic-build/scripts/test/test_property_reroute.py +165 -0
- package/skills/cosmic-build/scripts/test/test_property_schema_first.py +35 -0
- package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +221 -0
- package/skills/cosmic-build/scripts/test/test_register_plugins.py +76 -4
- package/skills/cosmic-build/scripts/test/test_render_output_cards.py +75 -3
- package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -5
- package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +103 -11
- package/skills/cosmic-build/scripts/test/test_report_plugin_source_gate.py +46 -16
- package/skills/cosmic-build/scripts/test/test_report_properties.py +74 -15
- package/skills/cosmic-build/scripts/test/test_report_query_data_source.py +3 -1
- package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +1 -2
- package/skills/cosmic-build/scripts/test/test_test_environment.py +76 -1
- package/skills/cosmic-build/scripts/test_environment.py +67 -1
- package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +52 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +16 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +59 -26
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +75 -18
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_auto.py +39 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_client.py +97 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +10 -12
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +38 -29
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +400 -233
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +87 -45
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +42 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +135 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +17 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_reroute.py +15 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +1429 -238
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/validators.py +56 -28
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +380 -134
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_export.py +12 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_verifier.py +42 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_add_field_placement_closure.py +8 -8
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_configuration_readback.py +94 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +47 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_metadata_unchanged_fail_closed.py +87 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_dym_evidence.py +23 -23
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_entity_integrity.py +279 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_field_guard.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +509 -32
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +178 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_recreate_plan.py +125 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_serve_contract.py +208 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +259 -50
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_inventory_perf.py +29 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +20 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +18 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +35 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_identity_guard.py +25 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_time_range_rule.py +98 -0
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/domain-model/report-model.md +20 -9
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/report-modeling-guide.md +55 -25
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/property-guides/report-column-properties.md +15 -39
- package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +19 -17
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +82 -38
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-batch.md +64 -19
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-plugin.md +32 -20
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md +78 -31
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/report-artifact-schema.json +2 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-companion-fields.md +3 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-list-configuration.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +27 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +99 -44
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_examples.py +31 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_validator.py +129 -29
- package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +6 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +104 -25
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +9 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +39 -12
- package/skills/cosmic-build/skills/mcp-server/SKILL.md +39 -24
- package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +20 -6
- package/skills/cosmic-dev-best-practices/SKILL.md +1 -1
- package/skills/cosmic-dev-best-practices/references/README.md +2 -2
- package/skills/cosmic-dev-best-practices/references/basedata-dev-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-document-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +13 -13
- package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/README.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +109 -13
- package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +1 -1
- package/skills/erp-analysis/references/delivery-contract.md +2 -1
- package/skills/erp-analysis/templates/AGENTS.template.md +3 -1
- package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +0 -169
- package/skills/cosmic-build/assets/platform-report-filters/platform.patch +0 -98
- package/skills/cosmic-build/scripts/apply_report_filter_extension.py +0 -39
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
"""Check Java plugin source against the DCS deployment gate before it leaves the workstation.
|
|
2
|
+
|
|
3
|
+
A customer packages the plugin and deploys it through DCS, whose code scan (Sonar, SpotBugs,
|
|
4
|
+
Fortify and Kingdee's own CVE-KD rules) rejects the package for problems the compiler never sees:
|
|
5
|
+
a static ``HashMap`` field, a log call inside a loop, a ``kd.bos.orm.ORM`` import, the same field
|
|
6
|
+
key literal written three times. Each rejection costs the customer a full rebuild and redeploy, so
|
|
7
|
+
the rules that a source scan can decide are checked here, while the code is still being written.
|
|
8
|
+
|
|
9
|
+
The rule codes are the DCS codes (``CVE-KD-0056-05``, ``java:S1192``, ``DM_NUMBER_CTOR`` ...) so a
|
|
10
|
+
finding here and a line in a DCS report name the same rule. Two levels:
|
|
11
|
+
|
|
12
|
+
* ``block`` — the source text alone settles the violation. ``compile_plugin_source.py`` refuses
|
|
13
|
+
the source, and the standalone scan exits 2.
|
|
14
|
+
* ``advise`` — a scan cannot prove it (types, data flow, estimated complexity). DCS may still
|
|
15
|
+
reject it, so each one is to be fixed or explicitly justified, but it does not block here.
|
|
16
|
+
|
|
17
|
+
This is a deliberately narrow reader, like ``java_source``: it works on comment- and literal-free
|
|
18
|
+
text and never claims a rule it cannot see. Rules that need the whole build — jar and class name
|
|
19
|
+
conflicts, metadata scans, null-flow analysis — stay with DCS; ``--source`` on a module directory
|
|
20
|
+
additionally covers the packaging rules it can see on disk (jar files, SQL scripts).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import bisect
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from java_source import _split_params, commentless, method_bodies, stripped
|
|
33
|
+
|
|
34
|
+
SCHEMA_VERSION = "app-build-dcs-code-gate/v1"
|
|
35
|
+
|
|
36
|
+
# DCS rule code -> (DCS risk, DCS rule name), copied from the DCS gate catalogue
|
|
37
|
+
# (金蝶AI套件公有云门禁) so a finding can be matched to the line DCS would report.
|
|
38
|
+
RULES: dict[str, tuple[str, str]] = {
|
|
39
|
+
"CVE-KD-0001": ("严重", "禁止使用指定类AccountUtils"),
|
|
40
|
+
"CVE-KD-0002": ("严重", "禁止使用指定类MCDBUtil"),
|
|
41
|
+
"CVE-KD-0003": ("严重", "禁止使用指定类MCApiUtil"),
|
|
42
|
+
"CVE-KD-0004": ("严重", "禁止使用指定类LoginMCServiceDbImpl"),
|
|
43
|
+
"CVE-KD-0005": ("严重", "禁止使用指定类DBConfig"),
|
|
44
|
+
"CVE-KD-0006": ("严重", "禁止使用指定类KDDriverUtil"),
|
|
45
|
+
"CVE-KD-0007": ("严重", "禁止使用指定类DataSourceFactory"),
|
|
46
|
+
"CVE-KD-0008": ("低危", "禁止调用System.getenv"),
|
|
47
|
+
"CVE-KD-0009": ("低危", "禁止调用System.getProperty"),
|
|
48
|
+
"CVE-KD-0010": ("低危", "禁止调用System.getProperties"),
|
|
49
|
+
"CVE-KD-0011-1": ("低危", "禁止二开通过反射操作,破坏苍穹架构约束Class.forName"),
|
|
50
|
+
"CVE-KD-0011-2": ("低危", "禁止二开通过反射操作,破坏苍穹架构约束java.lang.reflect"),
|
|
51
|
+
"CVE-KD-0011-3": ("低危", "禁止二开通过反射操作,破坏苍穹架构约束URLClassLoader"),
|
|
52
|
+
"CVE-KD-0012-1": ("低危", "禁止二开通过字节码ClassReader操作,破坏苍穹架构约束"),
|
|
53
|
+
"CVE-KD-0012-2": ("低危", "禁止二开通过字节码ClassWriter操作,破坏苍穹架构约束"),
|
|
54
|
+
"CVE-KD-0012-3": ("低危", "禁止二开通过字节码ClassVisitor操作,破坏苍穹架构约束"),
|
|
55
|
+
"CVE-KD-0012-4": ("低危", "禁止二开通过字节码操作,破坏苍穹架构约束javassist.CtClass"),
|
|
56
|
+
"CVE-KD-0012-5": ("低危", "禁止二开通过字节码操作,破坏苍穹架构约束ClassPool"),
|
|
57
|
+
"CVE-KD-0012-6": ("低危", "禁止二开通过字节码操作,破坏苍穹架构约束javassist.CtMethod"),
|
|
58
|
+
"CVE-KD-0012-7": ("低危", "禁止二开通过字节码操作,破坏苍穹架构约束Instrumentation"),
|
|
59
|
+
"CVE-KD-0012-8": ("低危", "禁止二开通过字节码操作,破坏苍穹架构约束ClassFileTransformer"),
|
|
60
|
+
"CVE-KD-0013-1": ("低危", "禁止关键字drop table"),
|
|
61
|
+
"CVE-KD-0013-2": ("低危", "禁止关键字truncate table"),
|
|
62
|
+
"CVE-KD-0013-3": ("低危", "禁止关键字rename table"),
|
|
63
|
+
"CVE-KD-0014-1": ("严重", "禁止使用指定类方法RequestContext.setAccountId"),
|
|
64
|
+
"CVE-KD-0014-2": ("严重", "禁止使用指定类方法RequestContext.setTenantId"),
|
|
65
|
+
"CVE-KD-0015-1": ("低危", "禁止使用SessionInfo所有set方法"),
|
|
66
|
+
"CVE-KD-0017": ("低危", "禁止调用System.setProperty"),
|
|
67
|
+
"CVE-KD-0018": ("严重", "禁止使用指定类RequestContextInfo"),
|
|
68
|
+
"CVE-KD-0019-1": ("低危", "Redis访问治理:禁止直接通过JedisClient访问Redis"),
|
|
69
|
+
"CVE-KD-0020": ("低危", "Redis访问治理:禁止直接通过Jedis客户端第三方包访问Redis"),
|
|
70
|
+
"CVE-KD-0021": ("低危", "Zookeeper访问治理:禁止直接通过ZKFactory访问Zookeeper"),
|
|
71
|
+
"CVE-KD-0022": ("低危", "Zookeeper访问治理:禁止直接通过Zookeeper客户端第三方包访问Zookeeper"),
|
|
72
|
+
"CVE-KD-0023": ("低危", "MQ访问治理:禁止直接通过RabbitMq客户端第三方包访问RabbiMq"),
|
|
73
|
+
"CVE-KD-0024": ("低危", "MQ访问治理:禁止直接通过RockettMq客户端第三方包访问RockettMq"),
|
|
74
|
+
"CVE-KD-0025": ("低危", "MQ访问治理:禁止直接通过kafka客户端第三方包访问kafka"),
|
|
75
|
+
"CVE-KD-0029": ("低危", "禁止二开使用Socket"),
|
|
76
|
+
"CVE-KD-0051-03": ("低危", "禁止在循环中访问kd.bos.cache.redis.RedisSessionlessCache"),
|
|
77
|
+
"CVE-KD-0051-04": ("低危", "禁止在循环中访问kd.bos.cache.redis.RedisSessionableCache"),
|
|
78
|
+
"CVE-KD-0051-07": ("低危", "禁止在循环中访问kd.bos.data.BusinessDataReader"),
|
|
79
|
+
"CVE-KD-0051-08": ("低危", "禁止在循环中访问kd.bos.data.BusinessDataWriter"),
|
|
80
|
+
"CVE-KD-0051-09": ("低危", "禁止在循环中访问kd.bos.servicehelper.operation.OperationServiceHelper"),
|
|
81
|
+
"CVE-KD-0051-10": ("低危", "禁止在循环中访问kd.bos.servicehelper.operation.SaveServiceHelper"),
|
|
82
|
+
"CVE-KD-0051-11": ("低危", "禁止在循环中访问kd.bos.servicehelper.operation.DeleteServiceHelper"),
|
|
83
|
+
"CVE-KD-TS-0051-46": ("高危", "禁止在循环中访问kd.bos.servicehelper.BusinessDataServiceHelper的loadHead方法"),
|
|
84
|
+
"CVE-KD-0052": ("高危", "禁止循环中访问view.updateView()"),
|
|
85
|
+
"CVE-KD-0053-01": ("严重", "关键字禁止select *"),
|
|
86
|
+
"CVE-KD-0054-1": ("低危", "禁止循环调用PageCache"),
|
|
87
|
+
"CVE-KD-0054-2": ("低危", "禁止循环调用RootPageCache"),
|
|
88
|
+
"CVE-KD-0055-1": ("低危", "二开禁止使用原生线程"),
|
|
89
|
+
"CVE-KD-0055-2": ("低危", "禁止使用指定类java.util.concurrent.FutureTask"),
|
|
90
|
+
"CVE-KD-0055-3": ("低危", "禁止使用指定类java.util.concurrent.ExecutorService"),
|
|
91
|
+
"CVE-KD-0055-4": ("低危", "禁止使用指定类java.util.concurrent.Executors"),
|
|
92
|
+
"CVE-KD-0055-5": ("低危", "禁止使用指定类java.util.concurrent.ThreadPoolExecutor"),
|
|
93
|
+
"CVE-KD-0055-6": ("低危", "禁止使用指定类java.util.concurrent.ForkJoinPool"),
|
|
94
|
+
"CVE-KD-0055-7": ("低危", "禁止使用指定类java.util.concurrent.ScheduledExecutorService"),
|
|
95
|
+
"CVE-KD-0055-8": ("低危", "禁止使用指定类java.util.concurrent.CompletableFuture"),
|
|
96
|
+
"CVE-KD-0055-9": ("低危", "禁止使用指定类java.util.concurrent.Future"),
|
|
97
|
+
"CVE-KD-0058-1": ("严重", "日志治理:禁止循环中写日志"),
|
|
98
|
+
"CVE-KD-0059-1": ("严重", "避免不合理地调用MetadataDao"),
|
|
99
|
+
"CVE-KD-0201": ("低危", "禁止循环调用微服务接口"),
|
|
100
|
+
"CVE-KD-0209-1": ("低危", "禁止使用System.out.print打印日志"),
|
|
101
|
+
"CVE-KD-0209-2": ("低危", "禁止使用e.printStackTrace()打印日志"),
|
|
102
|
+
"CVE-KD-0302-01": ("严重", "二开删除数据必须加有效过滤条件"),
|
|
103
|
+
"CVE-KD-0303": ("严重", "二开查询数据必须加有效过滤条件"),
|
|
104
|
+
"CVE-KD-0400": ("低危", "禁止调用kd.bos.lang.Lang.setHolder"),
|
|
105
|
+
"CVE-KD-0401": ("严重", "禁止通过注释跳过sonar检查(// NOSONAR)"),
|
|
106
|
+
"CVE-KD-0402": ("低危", "禁止通注解跳过sonar检查(@SuppressWarnings(\"squid:S243\"))"),
|
|
107
|
+
"CVE-KD-0403": ("中危", "包名,类名,方法名,变量名中文检测"),
|
|
108
|
+
"CVE-KD-0413": ("低危", "禁止使用kd.bos.db.DB"),
|
|
109
|
+
"CVE-KD-TS-0413": ("高危", "禁止使用kd.bos.db.DB"),
|
|
110
|
+
"CVE-KD-0414-2": ("严重", "禁止引用非标准SDK内容-新项目"),
|
|
111
|
+
"CVE-KD-0501": ("高危", "二开代码使用jdk17构建错误"),
|
|
112
|
+
"CVE-KD-0717-2": ("严重", "禁止二开直接删除数据——DeleteServiceHelper.delete()"),
|
|
113
|
+
"CVE-KD-0717-3": ("严重", "禁止二开直接删除数据——BusinessDataWriter.delete()"),
|
|
114
|
+
"CVE-KD-0717-4": ("严重", "禁止二开直接删除数据——ORM.delete()"),
|
|
115
|
+
"CVE-KD-0720-2": ("严重", "禁止使用CREATE OR REPLACE VIEW创建视图"),
|
|
116
|
+
"CVE-KD-0721-1": ("严重", "禁止使用CREATE PROCEDURE创建存储过程"),
|
|
117
|
+
"CVE-KD-0721-2": ("严重", "禁止使用CREATE OR REPLACE PROCEDURE创建存储过程"),
|
|
118
|
+
"CVE-KD-0722-1": ("严重", "禁止使用CREATE FUNCTION创建函数"),
|
|
119
|
+
"CVE-KD-0722-2": ("严重", "禁止使用CREATE OR REPLACE FUNCTION创建函数"),
|
|
120
|
+
"CVE-KD-0723-1": ("严重", "禁止使用CREATE TRIGGER创建触发器"),
|
|
121
|
+
"CVE-KD-0723-2": ("严重", "禁止使用CREATE OR REPLACE TRIGGER创建触发器"),
|
|
122
|
+
"CVE-KD-1215-1": ("高危", "禁止在两层及以上循环内读写数据库"),
|
|
123
|
+
"CVE-KD-1215-2": ("严重", "禁止在三层及以上循环内读写数据库"),
|
|
124
|
+
"CVE-KD-2026-0508": ("低危", "禁止使用kd.bos.orm.ORM"),
|
|
125
|
+
"CVE-KD-2026-0618": ("严重", "禁止在 propertyChanged() 中无条件调用 updateView()"),
|
|
126
|
+
"CVE-KD-20260901-1": ("严重", "禁止使用kd.bos.zk"),
|
|
127
|
+
"CVE-KD-20260901-2": ("严重", "禁止使用org.apache.zookeeper"),
|
|
128
|
+
"CVE-KD-20260901-3": ("严重", "禁止使用org.apache.curator"),
|
|
129
|
+
"CVE-KD-20260901-4": ("严重", "禁止使用kd.bos.mq.MessageConsumer"),
|
|
130
|
+
"CVE-KD-SCAN-jar": ("严重", "引入第三方jar包"),
|
|
131
|
+
"CVE-KD-SCAN-libFile": ("严重", "引入第三方文件"),
|
|
132
|
+
"CVE-KD-SQL-0001": ("严重", "禁止创建视图"),
|
|
133
|
+
"CVE-KD-SQL-0002": ("严重", "禁止创建存储过程"),
|
|
134
|
+
"CVE-KD-SQL-0003": ("严重", "禁止创建函数"),
|
|
135
|
+
"CVE-KD-SQL-0004": ("严重", "禁止创建触发器"),
|
|
136
|
+
"CVE-KD-SQL-0005": ("低危", "禁止插入数据"),
|
|
137
|
+
"CVE-KD-SQL-0006": ("严重", "禁止更新数据"),
|
|
138
|
+
"CVE-KD-SQL-0007": ("严重", "禁止删除数据"),
|
|
139
|
+
"CVE-KD-SQL-0008": ("严重", "禁止删除字段"),
|
|
140
|
+
"CVE-KD-SQL-0009": ("严重", "禁止删除表"),
|
|
141
|
+
"CVE-KD-SQL-0010": ("严重", "禁止重命名表和字段"),
|
|
142
|
+
"CVE-KD-SQL-0014": ("严重", "新建表名长度不可超过24个字符"),
|
|
143
|
+
"CVE-KD-SQL-0015": ("低危", "文件非UTF-8格式"),
|
|
144
|
+
"class_fully_qualified_name_conflict_stp": ("严重", "类完全限定名与标准产品冲突"),
|
|
145
|
+
"java:S100": ("低危", "Method names should comply with a naming convention"),
|
|
146
|
+
"java:S101": ("低危", "Class names should comply with a naming convention"),
|
|
147
|
+
"java:S106": ("中危", "Standard outputs should not be used directly to log anything"),
|
|
148
|
+
"java:S108": ("中危", "Nested blocks of code should not be left empty"),
|
|
149
|
+
"java:S112": ("中危", "Generic exceptions should never be thrown"),
|
|
150
|
+
"java:S115": ("高危", "Constant names should comply with a naming convention"),
|
|
151
|
+
"java:S116": ("低危", "Field names should comply with a naming convention"),
|
|
152
|
+
"java:S117": ("低危", "Local variable and method parameter names should comply with a naming convention"),
|
|
153
|
+
"java:S1066": ("中危", "Collapsible \"if\" statements should be merged"),
|
|
154
|
+
"java:S1068": ("中危", "Unused \"private\" fields should be removed"),
|
|
155
|
+
"java:S1118": ("中危", "Utility classes should not have public constructors"),
|
|
156
|
+
"java:S1124": ("低危", "Modifiers should be declared in the correct order"),
|
|
157
|
+
"java:S1125": ("低危", "Boolean literals should not be redundant"),
|
|
158
|
+
"java:S1126": ("低危", "Return of boolean expressions should not be wrapped into an \"if-then-else\" statement"),
|
|
159
|
+
"java:S1128": ("低危", "Unnecessary imports should be removed"),
|
|
160
|
+
"java:S1144": ("中危", "Unused \"private\" methods should be removed"),
|
|
161
|
+
"java:S1149": ("中危", "Synchronized classes Vector, Hashtable, Stack and StringBuffer should not be used"),
|
|
162
|
+
"java:S1153": ("低危", "String.valueOf() should not be appended to a String"),
|
|
163
|
+
"java:S1155": ("低危", "Collection.isEmpty() should be used to test for emptiness"),
|
|
164
|
+
"java:S1157": ("低危", "Case insensitive string comparisons should be made without intermediate upper or lower casing"),
|
|
165
|
+
"java:S1168": ("中危", "Empty arrays and collections should be returned instead of null"),
|
|
166
|
+
"java:S1171": ("中危", "Only static class initializers should be used"),
|
|
167
|
+
"java:S1172": ("中危", "Unused method parameters should be removed"),
|
|
168
|
+
"java:S1181": ("中危", "Throwable and Error should not be caught"),
|
|
169
|
+
"java:S1186": ("高危", "Methods should not be empty"),
|
|
170
|
+
"java:S1192": ("高危", "String literals should not be duplicated"),
|
|
171
|
+
"java:S1197": ("低危", "Array designators \"[]\" should be on the type, not the variable"),
|
|
172
|
+
"java:S1199": ("低危", "Nested code blocks should not be used"),
|
|
173
|
+
"java:S1206": ("低危", "\"equals(Object obj)\" and \"hashCode()\" should be overridden in pairs"),
|
|
174
|
+
"java:S1215": ("高危", "Execution of the Garbage Collector should be triggered only by the JVM"),
|
|
175
|
+
"java:S1220": ("低危", "The default unnamed package should not be used"),
|
|
176
|
+
"java:S125": ("中危", "Sections of code should not be commented out"),
|
|
177
|
+
"java:S128": ("严重", "Switch cases should end with an unconditional \"break\" statement"),
|
|
178
|
+
"java:S131": ("高危", "\"switch\" statements should have \"default\" clauses"),
|
|
179
|
+
"java:S1313": ("低危", "Using hardcoded IP addresses is security-sensitive"),
|
|
180
|
+
"java:S1444": ("低危", "\"public static\" fields should be constant"),
|
|
181
|
+
"java:S1452": ("高危", "Generic wildcard types should not be used in return types"),
|
|
182
|
+
"java:S1481": ("低危", "Unused local variables should be removed"),
|
|
183
|
+
"java:S1488": ("低危", "Local variables should not be declared and then immediately returned or thrown"),
|
|
184
|
+
"java:S1598": ("高危", "Package declaration should match source file directory"),
|
|
185
|
+
"java:S1604": ("中危", "Anonymous inner classes containing only one method should become lambdas"),
|
|
186
|
+
"java:S1643": ("低危", "Strings should not be concatenated using '+' in a loop"),
|
|
187
|
+
"java:S1845": ("严重", "Methods and field names should not be the same or differ only by capitalization"),
|
|
188
|
+
"java:S2068": ("严重", "Hard-coded credentials are security-sensitive"),
|
|
189
|
+
"java:S2077": ("中危", "Formatting SQL queries is security-sensitive"),
|
|
190
|
+
"java:S2111": ("中危", "\"BigDecimal(double)\" should not be used"),
|
|
191
|
+
"java:S2129": ("中危", "Constructors should not be used to instantiate \"String\", \"BigInteger\", \"BigDecimal\" and primitive-wrapper classes"),
|
|
192
|
+
"java:S2133": ("中危", "Objects should not be created only to \"getClass\""),
|
|
193
|
+
"java:S2139": ("中危", "Exceptions should be either logged or rethrown but not both"),
|
|
194
|
+
"java:S2142": ("中危", "\"InterruptedException\" should not be ignored"),
|
|
195
|
+
"java:S2168": ("严重", "Double-checked locking should not be used"),
|
|
196
|
+
"java:S2178": ("严重", "Short-circuit logic should be used in boolean contexts"),
|
|
197
|
+
"java:S2189": ("严重", "Loops should not be infinite"),
|
|
198
|
+
"java:S2245": ("高危", "Using pseudorandom number generators (PRNGs) is security-sensitive"),
|
|
199
|
+
"java:S2276": ("严重", "\"wait(...)\" should be used instead of \"Thread.sleep(...)\" when a lock is held"),
|
|
200
|
+
"java:S2386": ("低危", "Mutable fields should not be \"public static\""),
|
|
201
|
+
"java:S2696": ("高危", "Instance methods should not write to \"static\" fields"),
|
|
202
|
+
"java:S2755": ("严重", "XML parsers should not be vulnerable to XXE attacks"),
|
|
203
|
+
"java:S2864": ("中危", "\"entrySet()\" should be iterated when both the key and value are needed"),
|
|
204
|
+
"java:S3011": ("中危", "Reflection should not be used to increase accessibility of classes, methods, or fields"),
|
|
205
|
+
"java:S3358": ("中危", "Ternary operators should not be nested"),
|
|
206
|
+
"java:S3776": ("高危", "Cognitive Complexity of methods should not be too high"),
|
|
207
|
+
"java:S4423": ("高危", "Weak SSL/TLS protocols should not be used"),
|
|
208
|
+
"java:S4426": ("高危", "Cryptographic keys should be robust"),
|
|
209
|
+
"java:S4635": ("高危", "String offset-based methods should be preferred for finding substrings from offsets"),
|
|
210
|
+
"java:S4790": ("高危", "Using weak hashing algorithms is security-sensitive"),
|
|
211
|
+
"java:S4973": ("中危", "Strings and Boxed types should be compared using \"equals()\""),
|
|
212
|
+
"java:S5332": ("高危", "Using clear-text protocols is security-sensitive"),
|
|
213
|
+
"java:S5361": ("高危", "\"String#replace\" should be preferred to \"String#replaceAll\""),
|
|
214
|
+
"java:S5542": ("低危", "Encryption algorithms should be used with secure mode and padding scheme"),
|
|
215
|
+
"java:S5547": ("高危", "Cipher algorithms should be robust"),
|
|
216
|
+
"DM_BOOLEAN_CTOR": ("中危", "Performance - Method invokes inefficient Boolean constructor; use Boolean.valueOf(...) instead"),
|
|
217
|
+
"DM_CONVERT_CASE": ("低危", "I18n - Consider using Locale parameterized version of invoked method"),
|
|
218
|
+
"DM_EXIT": ("中危", "Bad practice - Method invokes System.exit(...)"),
|
|
219
|
+
"DM_FP_NUMBER_CTOR": ("中危", "Performance - Method invokes inefficient floating-point Number constructor; use static valueOf instead"),
|
|
220
|
+
"DM_GC": ("中危", "Performance - Explicit garbage collection; extremely dubious except in benchmarking code"),
|
|
221
|
+
"DM_NUMBER_CTOR": ("中危", "Performance - Method invokes inefficient Number constructor; use static valueOf instead"),
|
|
222
|
+
"DM_STRING_CTOR": ("中危", "Performance - Method invokes inefficient new String(String) constructor"),
|
|
223
|
+
"DM_STRING_VOID_CTOR": ("中危", "Performance - Method invokes inefficient new String() constructor"),
|
|
224
|
+
"EC_NULL_ARG": ("中危", "Correctness - Call to equals(null)"),
|
|
225
|
+
"ES_COMPARING_STRINGS_WITH_EQ": ("中危", "Bad practice - Comparison of String objects using == or !="),
|
|
226
|
+
"IIL_PATTERN_COMPILE_IN_LOOP": ("中危", "Performance - Method calls Pattern.compile in a loop"),
|
|
227
|
+
"ITA_INEFFICIENT_TO_ARRAY": ("中危", "Performance - Method uses toArray() with zero-length array argument"),
|
|
228
|
+
"WMI_WRONG_MAP_ITERATOR": ("中危", "Performance - Inefficient use of keySet iterator instead of entrySet iterator"),
|
|
229
|
+
"FORTIFY:Hardcoded Password": ("中危", "Password Management: Hardcoded Password / Empty Password"),
|
|
230
|
+
"FORTIFY:Hardcoded Encryption Key": ("严重", "Key Management: Hardcoded Encryption Key"),
|
|
231
|
+
"FORTIFY:Insecure Initialization Vector": ("严重", "Weak Encryption: Insecure Initialization Vector"),
|
|
232
|
+
"FORTIFY:Inadequate RSA Padding": ("中危", "Weak Encryption: Inadequate RSA Padding"),
|
|
233
|
+
"FORTIFY:Unsafe Deserialization": ("严重", "Dynamic Code Evaluation: Unsafe Deserialization"),
|
|
234
|
+
"FORTIFY:Return Inside Finally": ("高危", "Poor Error Handling: Return Inside Finally"),
|
|
235
|
+
"FORTIFY:Privacy Violation": ("中危", "Privacy Violation"),
|
|
236
|
+
"FORTIFY:SQL Injection": ("严重", "SQL Injection"),
|
|
237
|
+
"UUF_UNUSED_FIELD": ("中危", "Performance - Unused field"),
|
|
238
|
+
}
|
|
239
|
+
_STATIC_COLLECTIONS = {
|
|
240
|
+
"Collection": "01", "Map": "02", "Set": "03", "List": "04", "HashMap": "05", "LinkedHashMap": "06",
|
|
241
|
+
"Hashtable": "07", "TreeMap": "08", "WeakHashMap": "09", "IdentityHashMap": "10", "Properties": "11",
|
|
242
|
+
"HashSet": "12", "LinkedHashSet": "13", "TreeSet": "14", "ArrayList": "15", "LinkedList": "16",
|
|
243
|
+
"Vector": "17", "Stack": "18", "ConcurrentHashMap": "19", "ConcurrentSkipListMap": "20",
|
|
244
|
+
"CopyOnWriteArrayList": "21", "CopyOnWriteArraySet": "22", "ConcurrentSkipListSet": "23",
|
|
245
|
+
}
|
|
246
|
+
RULES.update({f"CVE-KD-0056-{number}": ("低危", f"禁止使用{name}静态集合变量") for name, number in _STATIC_COLLECTIONS.items()})
|
|
247
|
+
# Internal file-service and temporary-file keywords, each with its own DCS code.
|
|
248
|
+
_FILE_SERVICE_TOKENS = {
|
|
249
|
+
"attachmentServer.url": "1", "imageServer.url": "2", "attachment.fileserver": "3", "image.fileserver": "4",
|
|
250
|
+
"file/upload.do": "5", "file/remove.do": "6", "file/exists.do": "7", "file/getForbiddenExtensions.do": "8",
|
|
251
|
+
"file/getAllowExtensions.do": "9", "file/getFileSize.do": "10", "file/transferTempToPersist.do": "11",
|
|
252
|
+
"login/getTicket.do": "12", "tempfile/supportTempfile.do": "13", "tempfile/isSupported.do": "14",
|
|
253
|
+
"tempfile/isSupportTempTransToPersist.do": "15", "fileServerManage/getVersion.do": "16",
|
|
254
|
+
"fileServerManage/getDiskUrlInfos.do": "17", "fileServerManage/getMatchFileMsgInfos.do": "18",
|
|
255
|
+
"fileServerManage/getChildrenFileMsgInfos.do": "19", "fileServerManage/getMaxDirectoryScanDepth.do": "20",
|
|
256
|
+
"fileServerManage/getFileMsgInfo.do": "21", "fileServerManage/getFileMsgInfoTree.do": "22",
|
|
257
|
+
"fileServerManage/buildRmScript.do": "23", "fileServerManage/buildCopyScript.do": "24", "ereceipt.url": "25",
|
|
258
|
+
}
|
|
259
|
+
RULES.update({f"CVE-KD-0026-{number}": ("低危", f"文件访问治理:禁止使用特殊关键字{token}") for token, number in _FILE_SERVICE_TOKENS.items()})
|
|
260
|
+
_TEMPFILE_TOKENS = {"redis.serversForCache": "03", "tempfile.redis": "02", "tempfile.fileserver": "04", "tempfile.disk": "05"}
|
|
261
|
+
RULES.update({f"CVE-KD-0028-{number}": ("严重", f"附件临时路径检查方式不正确{token}") for token, number in _TEMPFILE_TOKENS.items()})
|
|
262
|
+
_RISK_ORDER = {"严重": 0, "高危": 1, "中危": 2, "低危": 3}
|
|
263
|
+
# 客户两表按真实编号取并集;运行时不依赖客户本机的 Excel 或 Office 库。
|
|
264
|
+
CATALOG = json.loads(Path(__file__).with_name("dcs-rule-catalog.json").read_text(encoding="utf-8"))
|
|
265
|
+
for _code, _entry in CATALOG["rules"].items():
|
|
266
|
+
RULES[_code] = (_entry["risk"], _entry["name"] or RULES.get(_code, ("", _code))[1])
|
|
267
|
+
|
|
268
|
+
# 旧 Fortify 名称是兼容别名。具体 UUID 可能对应不同语言/调用链,作为关联编号,
|
|
269
|
+
# 不冒充已定位到某个 Fortify 检测器;实际匹配仍由 DCS 完整分析决定。
|
|
270
|
+
_FORTIFY_NAMES = {
|
|
271
|
+
"FORTIFY:Hardcoded Password": "Password Management: Hardcoded Password",
|
|
272
|
+
"FORTIFY:Hardcoded Encryption Key": "Key Management: Hardcoded Encryption Key",
|
|
273
|
+
"FORTIFY:Insecure Initialization Vector": "Weak Encryption: Insecure Initialization Vector",
|
|
274
|
+
"FORTIFY:Inadequate RSA Padding": "Weak Encryption: Inadequate RSA Padding",
|
|
275
|
+
"FORTIFY:Unsafe Deserialization": "Dynamic Code Evaluation: Unsafe Deserialization",
|
|
276
|
+
"FORTIFY:Return Inside Finally": "Poor Error Handling: Return Inside Finally",
|
|
277
|
+
"FORTIFY:Privacy Violation": "Privacy Violation",
|
|
278
|
+
"FORTIFY:SQL Injection": "SQL Injection",
|
|
279
|
+
"java:S2755": "XML External Entity Injection",
|
|
280
|
+
"java:S4426": "Weak Encryption: Insufficient Key Size",
|
|
281
|
+
"java:S4830": "Insecure SSL: Overly Broad Certificate Trust",
|
|
282
|
+
"java:S5527": "Insecure SSL: Server Identity Verification Disabled",
|
|
283
|
+
}
|
|
284
|
+
_FORTIFY_CODES = {
|
|
285
|
+
alias: tuple(code for code, entry in CATALOG["rules"].items() if entry["engine"] == "fortify" and entry["name"] == name)
|
|
286
|
+
for alias, name in _FORTIFY_NAMES.items()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
BLOCK = "block"
|
|
290
|
+
ADVISE = "advise"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _Source:
|
|
294
|
+
"""One Java file in three aligned views, plus the structure every rule needs.
|
|
295
|
+
|
|
296
|
+
``raw`` is the file, ``code`` has comments and literals blanked, ``lit`` keeps literals. All
|
|
297
|
+
three share offsets, so a match in any of them reports the same line.
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(self, text: str, path: Path | None = None) -> None:
|
|
301
|
+
self.raw = text
|
|
302
|
+
self.path = path
|
|
303
|
+
self.code = stripped(text)
|
|
304
|
+
self.lit = commentless(text)
|
|
305
|
+
self._starts = [0] + [index + 1 for index, char in enumerate(text) if char == "\n"]
|
|
306
|
+
self.pair: dict[int, int] = {}
|
|
307
|
+
self.opener: dict[int, int] = {}
|
|
308
|
+
self.owners: dict[int, str] = {}
|
|
309
|
+
self._braces: list[tuple[int, int]] = []
|
|
310
|
+
stack: list[int] = []
|
|
311
|
+
depth = 0
|
|
312
|
+
for index, char in enumerate(self.code):
|
|
313
|
+
if char in "({[":
|
|
314
|
+
stack.append(index)
|
|
315
|
+
elif char in ")}]" and stack:
|
|
316
|
+
start = stack.pop()
|
|
317
|
+
self.pair[start] = index
|
|
318
|
+
self.opener[index] = start
|
|
319
|
+
if char in "{}":
|
|
320
|
+
depth += 1 if char == "{" else -1
|
|
321
|
+
self._braces.append((index, depth))
|
|
322
|
+
self.loops = self._loop_spans()
|
|
323
|
+
self.methods = self._methods()
|
|
324
|
+
self.literals = [(match.start(), match.group(1)) for match in _STRING_RE.finditer(self.lit)
|
|
325
|
+
if match.group(0).startswith('"')]
|
|
326
|
+
|
|
327
|
+
def line(self, offset: int) -> int:
|
|
328
|
+
return bisect.bisect_right(self._starts, offset)
|
|
329
|
+
|
|
330
|
+
def brace_depth(self, offset: int) -> int:
|
|
331
|
+
"""How many braces are open just before ``offset``."""
|
|
332
|
+
position = bisect.bisect_left(self._braces, (offset, -(10 ** 9)))
|
|
333
|
+
return self._braces[position - 1][1] if position else 0
|
|
334
|
+
|
|
335
|
+
def statement_end(self, index: int) -> int:
|
|
336
|
+
code = self.code
|
|
337
|
+
while index < len(code):
|
|
338
|
+
char = code[index]
|
|
339
|
+
if char in "({[" and index in self.pair:
|
|
340
|
+
index = self.pair[index] + 1
|
|
341
|
+
continue
|
|
342
|
+
if char in ";}":
|
|
343
|
+
return index
|
|
344
|
+
index += 1
|
|
345
|
+
return len(code)
|
|
346
|
+
|
|
347
|
+
def body_after(self, index: int) -> tuple[int, int] | None:
|
|
348
|
+
"""The span of the statement or block that starts at the first code after ``index``."""
|
|
349
|
+
code = self.code
|
|
350
|
+
while index < len(code) and code[index].isspace():
|
|
351
|
+
index += 1
|
|
352
|
+
if index >= len(code) or code[index] == ";":
|
|
353
|
+
return None
|
|
354
|
+
if code[index] == "{":
|
|
355
|
+
close = self.pair.get(index)
|
|
356
|
+
return (index, close) if close is not None else None
|
|
357
|
+
return index, self.statement_end(index)
|
|
358
|
+
|
|
359
|
+
def _loop_spans(self) -> list[tuple[int, int]]:
|
|
360
|
+
spans = []
|
|
361
|
+
for match in re.finditer(r"\b(?:for|while)\s*\(", self.code):
|
|
362
|
+
close = self.pair.get(match.end() - 1)
|
|
363
|
+
body = self.body_after(close + 1) if close is not None else None
|
|
364
|
+
if body:
|
|
365
|
+
spans.append(body)
|
|
366
|
+
for match in re.finditer(r"\bdo\s*\{", self.code):
|
|
367
|
+
close = self.pair.get(match.end() - 1)
|
|
368
|
+
if close is not None:
|
|
369
|
+
spans.append((match.end() - 1, close))
|
|
370
|
+
for match in re.finditer(r"\.\s*forEach\s*\(", self.code):
|
|
371
|
+
close = self.pair.get(match.end() - 1)
|
|
372
|
+
if close is not None:
|
|
373
|
+
spans.append((match.end() - 1, close))
|
|
374
|
+
return spans
|
|
375
|
+
|
|
376
|
+
def loop_depth(self, offset: int) -> int:
|
|
377
|
+
return sum(1 for start, end in self.loops if start < offset < end)
|
|
378
|
+
|
|
379
|
+
def _methods(self) -> list[dict]:
|
|
380
|
+
methods = []
|
|
381
|
+
for body in method_bodies(self.raw):
|
|
382
|
+
opening = body["start"] - 1
|
|
383
|
+
head_start = max(self.code.rfind(char, 0, opening) for char in ";{}") + 1
|
|
384
|
+
header = self.code[head_start:opening]
|
|
385
|
+
# The declared name is the word before the last top-level parameter list; method_bodies
|
|
386
|
+
# takes the first `name(`, which is a type annotation in `Result<@Ann("x") T> save(...)`.
|
|
387
|
+
tail = re.sub(r"\bthrows\s+[\w$.,\s<>]+$", "", header.rstrip()).rstrip()
|
|
388
|
+
if not tail.endswith(")") or head_start + len(tail) - 1 not in self.opener:
|
|
389
|
+
continue
|
|
390
|
+
params_close = head_start + len(tail) - 1
|
|
391
|
+
params_open = self.opener[params_close]
|
|
392
|
+
index = params_open - 1
|
|
393
|
+
while index >= head_start and self.code[index].isspace():
|
|
394
|
+
index -= 1
|
|
395
|
+
name = _word_ending_at(self.code, index)
|
|
396
|
+
if not name:
|
|
397
|
+
continue
|
|
398
|
+
name_offset = index + 1 - len(name)
|
|
399
|
+
prefix = self.code[head_start:name_offset]
|
|
400
|
+
methods.append({
|
|
401
|
+
"name": name,
|
|
402
|
+
"start": body["start"],
|
|
403
|
+
"end": body["end"],
|
|
404
|
+
"headStart": head_start,
|
|
405
|
+
"nameOffset": name_offset,
|
|
406
|
+
"prefix": prefix,
|
|
407
|
+
"params": self.code[params_open + 1 : params_close],
|
|
408
|
+
"static": bool(re.search(r"\bstatic\b", prefix)),
|
|
409
|
+
"private": bool(re.search(r"\bprivate\b", prefix)),
|
|
410
|
+
"override": "@Override" in prefix,
|
|
411
|
+
"synchronized": bool(re.search(r"\bsynchronized\b", prefix)),
|
|
412
|
+
})
|
|
413
|
+
return methods
|
|
414
|
+
|
|
415
|
+
def method_at(self, offset: int) -> dict | None:
|
|
416
|
+
return next((item for item in self.methods if item["start"] <= offset <= item["end"]), None)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
_STRING_RE = re.compile(r"'(?:\\.|[^'\\\n])*'|\"((?:\\.|[^\"\\\n])*)\"")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _issue(src: _Source, offset: int, rules, message: str, fix: str, level: str = BLOCK) -> dict:
|
|
423
|
+
codes = [rules] if isinstance(rules, str) else list(rules)
|
|
424
|
+
related = sorted({code for alias in codes for code in _FORTIFY_CODES.get(alias, ())})
|
|
425
|
+
risk = min((RULES[code][0] for code in codes + related), key=_RISK_ORDER.__getitem__)
|
|
426
|
+
return {
|
|
427
|
+
"line": src.line(offset),
|
|
428
|
+
"ruleCodes": codes,
|
|
429
|
+
"relatedRuleCodes": related,
|
|
430
|
+
"dcsRisk": risk,
|
|
431
|
+
"dcsRule": RULES[codes[0]][1],
|
|
432
|
+
"level": level,
|
|
433
|
+
"message": message,
|
|
434
|
+
"fix": fix,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _typed_names(src: _Source, types: str) -> set[str]:
|
|
439
|
+
"""Variables and fields declared with one of the given simple type names."""
|
|
440
|
+
pattern = rf"\b(?:{types})\s*(?:<[^;=(){{}}]*>)?\s+([A-Za-z_$][\w$]*)\s*[=;,)]"
|
|
441
|
+
return {match.group(1) for match in re.finditer(pattern, src.code)}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _receiver_calls(src: _Source, types: str, extra: tuple[str, ...] = ()):
|
|
445
|
+
"""按调用位置核对变量绑定;同名参数、局部变量和字段不跨作用域继承类型。"""
|
|
446
|
+
names = _typed_names(src, types) | set(extra)
|
|
447
|
+
alternatives = [types] + (["|".join(re.escape(name) for name in sorted(names))] if names else [])
|
|
448
|
+
pattern = rf"(?<![\w$.])(?:this\s*\.\s*)?(?:{'|'.join(alternatives)})\s*\.\s*([A-Za-z_$][\w$]*)\s*\("
|
|
449
|
+
bindings = []
|
|
450
|
+
for declared in re.finditer(r"\b([\w$.]+(?:\s*<[^;=(){}]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_$][\w$]*)\s*(?=[=;,):])", src.code):
|
|
451
|
+
if declared.group(1) in _NOT_TYPES:
|
|
452
|
+
continue
|
|
453
|
+
braces = [(opening, end) for opening, end in src.pair.items() if src.code[opening] == "{" and opening < declared.start() < end]
|
|
454
|
+
if not braces:
|
|
455
|
+
continue
|
|
456
|
+
opening, end = max(braces)
|
|
457
|
+
head = src.code[max(src.code.rfind(char, 0, opening) for char in ";{}") + 1:opening]
|
|
458
|
+
field = bool(re.search(r"\b(?:class|interface|enum|record)\s+\w+|\bnew\s+[\w$.]+(?:<[^;(){}]*>)?\s*\([^;{}]*\)\s*$", head))
|
|
459
|
+
parens = [(start, close) for start, close in src.pair.items() if src.code[start] == "(" and opening < start < declared.start() < close]
|
|
460
|
+
if parens:
|
|
461
|
+
start, close = max(parens)
|
|
462
|
+
tail = re.match(r"\s*(?:throws\s+[\w$.,\s]+)?(?:->\s*)?", src.code[close + 1:])
|
|
463
|
+
scope = src.body_after(close + 1 + tail.end())
|
|
464
|
+
if scope is None:
|
|
465
|
+
continue
|
|
466
|
+
opening, end = scope
|
|
467
|
+
field = False
|
|
468
|
+
bindings.append((opening, end, declared.start(), field, re.sub(r"\s+", "", declared.group(1)), declared.group(2)))
|
|
469
|
+
for call in re.finditer(pattern, src.code):
|
|
470
|
+
receiver = re.sub(r"\s+", "", src.code[call.start():call.start(1)]).rstrip(".")
|
|
471
|
+
explicit = receiver.startswith("this.")
|
|
472
|
+
name = receiver.removeprefix("this.")
|
|
473
|
+
visible = [binding for binding in bindings if binding[5] == name and binding[0] < call.start() < binding[1]
|
|
474
|
+
and (binding[3] or binding[2] < call.start()) and (not explicit or binding[3])]
|
|
475
|
+
if visible:
|
|
476
|
+
binding = max(visible, key=lambda item: (item[0], item[2]))
|
|
477
|
+
if not re.fullmatch(types, binding[4]):
|
|
478
|
+
continue
|
|
479
|
+
# 嵌套函数的参数或类型未能解析时,不能沿用外层绑定作阻断证明。
|
|
480
|
+
method = src.method_at(call.start())
|
|
481
|
+
boundary = max(binding[0] + 1, method["start"] if method else 0)
|
|
482
|
+
if not explicit and _nested_function(src, call.start(), boundary):
|
|
483
|
+
continue
|
|
484
|
+
if not explicit and any(call.start() < src.statement_end(lam.end()) for lam in re.finditer(rf"\b{re.escape(name)}\s*->", src.code[:call.start()])):
|
|
485
|
+
continue
|
|
486
|
+
yield call
|
|
487
|
+
continue
|
|
488
|
+
if not explicit and (re.fullmatch(types, name) or name in extra):
|
|
489
|
+
yield call
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# --- Forbidden platform internals, middleware clients, threads, reflection -------------------
|
|
493
|
+
|
|
494
|
+
# (rule codes, targets, replacement). A target ending in "." is a package; a dotted name is a
|
|
495
|
+
# class matched by import, by fully qualified use, or by simple name under a wildcard import; a
|
|
496
|
+
# bare name is a class whose package DCS does not state, matched by simple name.
|
|
497
|
+
_FORBIDDEN_TYPES = (
|
|
498
|
+
("CVE-KD-0001", ("AccountUtils",), "read tenant/account data through the documented RequestContext getters"),
|
|
499
|
+
("CVE-KD-0002", ("MCDBUtil",), "use the platform service helpers; MC database access is not open to customization"),
|
|
500
|
+
("CVE-KD-0003", ("MCApiUtil",), "use the platform service helpers; MC APIs are not open to customization"),
|
|
501
|
+
("CVE-KD-0004", ("LoginMCServiceDbImpl",), "use the documented login/session APIs"),
|
|
502
|
+
("CVE-KD-0005", ("DBConfig",), "use QueryServiceHelper / BusinessDataServiceHelper; datasource configuration is internal"),
|
|
503
|
+
("CVE-KD-0006", ("KDDriverUtil",), "use QueryServiceHelper / BusinessDataServiceHelper; JDBC drivers are internal"),
|
|
504
|
+
("CVE-KD-0007", ("DataSourceFactory",), "use QueryServiceHelper / BusinessDataServiceHelper; datasources are internal"),
|
|
505
|
+
("CVE-KD-0018", ("RequestContextInfo",), "read the current context through RequestContext.get() getters only"),
|
|
506
|
+
("CVE-KD-0019-1", ("JedisClient",), "use the platform cache (CacheFactory distributed cache) instead of Redis clients"),
|
|
507
|
+
("CVE-KD-0020", ("redis.clients.",), "use the platform cache (CacheFactory distributed cache) instead of Jedis"),
|
|
508
|
+
("CVE-KD-0021", ("ZKFactory",), "do not access Zookeeper from customization; use platform locks/configuration services"),
|
|
509
|
+
("CVE-KD-0022", ("org.apache.zookeeper.",), "do not access Zookeeper from customization"),
|
|
510
|
+
("CVE-KD-20260901-2", ("org.apache.zookeeper.",), "do not access Zookeeper from customization"),
|
|
511
|
+
("CVE-KD-20260901-3", ("org.apache.curator.",), "do not access Zookeeper from customization"),
|
|
512
|
+
("CVE-KD-20260901-1", ("kd.bos.zk.",), "do not use kd.bos.zk; use platform locks/configuration services"),
|
|
513
|
+
("CVE-KD-20260901-4", ("kd.bos.mq.MessageConsumer",), "do not consume MQ directly; use a platform-registered scheduled task or event subscription"),
|
|
514
|
+
("CVE-KD-0023", ("com.rabbitmq.",), "do not use MQ clients directly from customization"),
|
|
515
|
+
("CVE-KD-0024", ("org.apache.rocketmq.",), "do not use MQ clients directly from customization"),
|
|
516
|
+
("CVE-KD-0025", ("org.apache.kafka.",), "do not use MQ clients directly from customization"),
|
|
517
|
+
("CVE-KD-0029", ("java.net.Socket", "java.net.ServerSocket", "java.net.DatagramSocket",
|
|
518
|
+
"java.nio.channels.SocketChannel", "java.nio.channels.ServerSocketChannel"),
|
|
519
|
+
"call external systems through the platform integration/HTTP facilities, never raw sockets"),
|
|
520
|
+
("CVE-KD-0413", ("kd.bos.db.DB",), "use QueryServiceHelper (query/queryDataSet) or BusinessDataServiceHelper; KSQL through DB is not allowed"),
|
|
521
|
+
("CVE-KD-2026-0508", ("kd.bos.orm.ORM",), "QueryServiceHelper.queryDataSet(algoKey, entity, fields, filters, orderBy) takes the same arguments as ORM.queryDataSet; use QueryServiceHelper.query/exists or BusinessDataServiceHelper for the rest"),
|
|
522
|
+
("CVE-KD-0011-2", ("java.lang.reflect.",), "call the API directly; reflection breaks platform constraints"),
|
|
523
|
+
("CVE-KD-0011-3", ("java.net.URLClassLoader",), "do not load classes dynamically from customization"),
|
|
524
|
+
("CVE-KD-0012-1", ("ClassReader",), "do not manipulate bytecode from customization"),
|
|
525
|
+
("CVE-KD-0012-2", ("ClassWriter",), "do not manipulate bytecode from customization"),
|
|
526
|
+
("CVE-KD-0012-3", ("ClassVisitor",), "do not manipulate bytecode from customization"),
|
|
527
|
+
("CVE-KD-0012-4", ("javassist.",), "do not manipulate bytecode from customization"),
|
|
528
|
+
("CVE-KD-0012-5", ("ClassPool",), "do not manipulate bytecode from customization"),
|
|
529
|
+
("CVE-KD-0012-6", ("CtMethod",), "do not manipulate bytecode from customization"),
|
|
530
|
+
("CVE-KD-0012-7", ("java.lang.instrument.Instrumentation",), "do not instrument classes from customization"),
|
|
531
|
+
("CVE-KD-0012-8", ("java.lang.instrument.ClassFileTransformer",), "do not instrument classes from customization"),
|
|
532
|
+
("CVE-KD-0055-2", ("java.util.concurrent.FutureTask",), "run asynchronous work with kd.bos.threads.ThreadPools.executeOnce(name, runnable)"),
|
|
533
|
+
("CVE-KD-0055-3", ("java.util.concurrent.ExecutorService",), "use kd.bos.threads.ThreadPools.executeOnce(name, runnable) or a kd.bos.threads.ThreadPool; do not hold an ExecutorService"),
|
|
534
|
+
("CVE-KD-0055-4", ("java.util.concurrent.Executors",), "use kd.bos.threads.ThreadPools instead of Executors"),
|
|
535
|
+
("CVE-KD-0055-5", ("java.util.concurrent.ThreadPoolExecutor",), "use kd.bos.threads.ThreadPools instead of ThreadPoolExecutor"),
|
|
536
|
+
("CVE-KD-0055-6", ("java.util.concurrent.ForkJoinPool",), "use kd.bos.threads.ThreadPools instead of ForkJoinPool"),
|
|
537
|
+
("CVE-KD-0055-7", ("java.util.concurrent.ScheduledExecutorService",), "use a platform scheduled task instead of a ScheduledExecutorService"),
|
|
538
|
+
("CVE-KD-0055-8", ("java.util.concurrent.CompletableFuture",), "use kd.bos.threads.ThreadPools.executeOnce(name, runnable) instead of CompletableFuture"),
|
|
539
|
+
("CVE-KD-0055-9", ("java.util.concurrent.Future",), "use kd.bos.threads.ThreadPools.executeOnce(name, runnable); do not wait on a Future"),
|
|
540
|
+
("CVE-KD-0501", ("sun.", "com.sun.", "javax.xml.bind.", "javax.xml.ws.", "javax.jws.", "javax.activation.",
|
|
541
|
+
"javax.annotation.PostConstruct", "javax.annotation.PreDestroy"),
|
|
542
|
+
"JDK internal or removed APIs fail the JDK 17 build; use java.util.Base64, java.time and other public JDK APIs"),
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
_IMPORT_RE = re.compile(r"^[ \t]*import\s+(static\s+)?([\w.$]+?)(\.\*)?\s*;", re.MULTILINE)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _imports(src: _Source) -> list[tuple[int, str, bool, bool]]:
|
|
549
|
+
return [(match.start(2), match.group(2), bool(match.group(1)), bool(match.group(3)))
|
|
550
|
+
for match in _IMPORT_RE.finditer(src.code)]
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _forbidden_type_issues(src: _Source) -> list[dict]:
|
|
554
|
+
issues = []
|
|
555
|
+
imports = _imports(src)
|
|
556
|
+
body = _IMPORT_RE.sub(lambda match: " " * len(match.group(0)), src.code)
|
|
557
|
+
for code, targets, fix in _FORBIDDEN_TYPES:
|
|
558
|
+
hit = None
|
|
559
|
+
for target in targets:
|
|
560
|
+
if target.endswith("."):
|
|
561
|
+
hit = next((offset for offset, name, _static, _wild in imports if (name + ".").startswith(target)), None)
|
|
562
|
+
if hit is None:
|
|
563
|
+
match = re.search(rf"(?<![\w.]){re.escape(target)}\w", body)
|
|
564
|
+
hit = match.start() if match else None
|
|
565
|
+
elif "." in target:
|
|
566
|
+
package, simple = target.rsplit(".", 1)
|
|
567
|
+
hit = next((offset for offset, name, static, wild in imports
|
|
568
|
+
if name == target or (static and name.startswith(target + "."))
|
|
569
|
+
or (wild and name == package and re.search(rf"\b{simple}\b", body))), None)
|
|
570
|
+
if hit is None:
|
|
571
|
+
match = re.search(rf"(?<![\w.]){re.escape(target)}\b", body)
|
|
572
|
+
hit = match.start() if match else None
|
|
573
|
+
else:
|
|
574
|
+
match = re.search(rf"(?<![\w$]){re.escape(target)}(?![\w$])", src.code)
|
|
575
|
+
hit = match.start() if match else None
|
|
576
|
+
if hit is not None:
|
|
577
|
+
break
|
|
578
|
+
if hit is not None:
|
|
579
|
+
issues.append(_issue(src, hit, code, f"forbidden by DCS: {RULES[code][1]}", fix))
|
|
580
|
+
return issues
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
# Calls forbidden anywhere: (pattern over code, rule codes, message, fix).
|
|
584
|
+
_FORBIDDEN_CALLS = (
|
|
585
|
+
(r"\bSystem\s*\.\s*getenv\s*\(", "CVE-KD-0008", "System.getenv", "read configuration through the platform parameter/configuration services"),
|
|
586
|
+
(r"\bSystem\s*\.\s*getProperty\s*\(", "CVE-KD-0009", "System.getProperty", "read configuration through the platform parameter/configuration services"),
|
|
587
|
+
(r"\bSystem\s*\.\s*getProperties\s*\(", "CVE-KD-0010", "System.getProperties", "read configuration through the platform parameter/configuration services"),
|
|
588
|
+
(r"\bSystem\s*\.\s*setProperty\s*\(", "CVE-KD-0017", "System.setProperty", "never change JVM-wide state from a plugin"),
|
|
589
|
+
(r"\bClass\s*\.\s*forName\s*\(", "CVE-KD-0011-1", "Class.forName", "reference the class directly"),
|
|
590
|
+
(r"\.\s*getDeclared(?:Method|Field|Constructor)s?\s*\(", "CVE-KD-0011-2", "reflective member lookup", "call the API directly"),
|
|
591
|
+
(r"\.\s*setAccessible\s*\(", ("java:S3011", "CVE-KD-0011-2"), "setAccessible bypasses access control", "use the public API"),
|
|
592
|
+
(r"\bLang\s*\.\s*setHolder\s*\(", "CVE-KD-0400", "Lang.setHolder", "never change the request language holder"),
|
|
593
|
+
(r"\bRequestContext\b[\s\S]{0,80}?\.\s*setAccountId\s*\(", "CVE-KD-0014-1", "RequestContext.setAccountId", "never switch the account of the current request"),
|
|
594
|
+
(r"\bRequestContext\b[\s\S]{0,80}?\.\s*setTenantId\s*\(", "CVE-KD-0014-2", "RequestContext.setTenantId", "never switch the tenant of the current request"),
|
|
595
|
+
(r"\bSessionInfo\s*\.\s*get\s*\(\s*\)\s*\.\s*set\w*\s*\(", "CVE-KD-0015-1", "SessionInfo setter", "never modify the session"),
|
|
596
|
+
(r"\bSystem\s*\.\s*(?:out|err)\s*\.\s*print", ("CVE-KD-0209-1", "java:S106"), "System.out/err printing",
|
|
597
|
+
"log through kd.bos.logging.Log: private static final Log LOG = LogFactory.getLog(X.class)"),
|
|
598
|
+
(r"\.\s*printStackTrace\s*\(", "CVE-KD-0209-2", "printStackTrace()", "log with LOG.error(\"business message\", e) or rethrow with the cause"),
|
|
599
|
+
(r"\bSystem\s*\.\s*gc\s*\(|\bRuntime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*gc\s*\(", ("java:S1215", "DM_GC"), "explicit garbage collection", "remove the call"),
|
|
600
|
+
(r"\bSystem\s*\.\s*exit\s*\(|\bRuntime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*(?:exit|halt)\s*\(", "DM_EXIT", "System.exit", "throw a KDBizException instead of stopping the JVM"),
|
|
601
|
+
(r"\bnew\s+(?:java\.lang\.)?Thread\s*\(|\bextends\s+(?:java\.lang\.)?Thread\b", "CVE-KD-0055-1", "native thread", "run asynchronous work with kd.bos.threads.ThreadPools.executeOnce(name, runnable)"),
|
|
602
|
+
(r"\bnew\s+(?:java\.util\.)?Timer\s*\(", "CVE-KD-0055-1", "java.util.Timer starts a native thread", "use a platform scheduled task"),
|
|
603
|
+
(r"\bMetadataDao\s*\.", "CVE-KD-0059-1", "MetadataDao", "read runtime metadata through EntityMetadataCache.getDataEntityType(entityNumber)"),
|
|
604
|
+
(r"\bDeleteServiceHelper\s*\.\s*delete\s*\(", "CVE-KD-0717-2", "DeleteServiceHelper.delete deletes rows directly",
|
|
605
|
+
"delete through the operation: OperationServiceHelper.executeOperate(\"delete\", entityNumber, ids, OperateOption.create()) so validations and plugins run"),
|
|
606
|
+
(r"\bBusinessDataWriter\s*\.\s*delete\s*\(", "CVE-KD-0717-3", "BusinessDataWriter.delete deletes rows directly",
|
|
607
|
+
"delete through OperationServiceHelper.executeOperate(\"delete\", ...)"),
|
|
608
|
+
(r"\bORM\b[^;]*?\.\s*delete\s*\(", "CVE-KD-0717-4", "ORM.delete deletes rows directly",
|
|
609
|
+
"delete through OperationServiceHelper.executeOperate(\"delete\", ...)"),
|
|
610
|
+
(r"\bnew\s+(?:java\.io\.)?ObjectInputStream\s*\(|\bnew\s+(?:java\.beans\.)?XMLDecoder\s*\(|setAutoTypeSupport\s*\(\s*true",
|
|
611
|
+
"FORTIFY:Unsafe Deserialization", "unsafe deserialization", "exchange data as JSON into explicit types"),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _forbidden_call_issues(src: _Source) -> list[dict]:
|
|
616
|
+
issues = []
|
|
617
|
+
for pattern, codes, what, fix in _FORBIDDEN_CALLS:
|
|
618
|
+
for match in re.finditer(pattern, src.code):
|
|
619
|
+
issues.append(_issue(src, match.start(), codes, f"{what} is forbidden by DCS", fix))
|
|
620
|
+
for name in _typed_names(src, "SessionInfo"):
|
|
621
|
+
for match in re.finditer(rf"(?<![\w$.]){re.escape(name)}\s*\.\s*set\w*\s*\(", src.code):
|
|
622
|
+
issues.append(_issue(src, match.start(), "CVE-KD-0015-1", "SessionInfo setter is forbidden by DCS", "never modify the session"))
|
|
623
|
+
return issues
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# --- 客户新增的指定调用和高风险源码检查 -------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _catalogue_issues(src: _Source) -> list[dict]:
|
|
630
|
+
issues = []
|
|
631
|
+
for code, entry in CATALOG["rules"].items():
|
|
632
|
+
check = entry.get("check", {})
|
|
633
|
+
if entry["engine"] != "sonar" or check.get("kind") not in ("循环里面指定方法", "指定类中指定方法"):
|
|
634
|
+
continue
|
|
635
|
+
# 表中多方法配置仅首项带全限定类名,后续项为同类方法名。
|
|
636
|
+
target, *extra = check["target"].split(",")
|
|
637
|
+
if "." not in target:
|
|
638
|
+
continue
|
|
639
|
+
owner, method = target.rsplit(".", 1)
|
|
640
|
+
simple = owner.rsplit(".", 1)[-1]
|
|
641
|
+
for call in _receiver_calls(src, re.escape(owner) + "|" + re.escape(simple)):
|
|
642
|
+
if call.group(1) not in (method, *extra):
|
|
643
|
+
continue
|
|
644
|
+
if check["kind"] == "循环里面指定方法" and not src.loop_depth(call.start()):
|
|
645
|
+
continue
|
|
646
|
+
issues.append(_issue(src, call.start(), code, f"{owner}.{call.group(1)}: {entry['name']}",
|
|
647
|
+
_LOOP_FIX if check["kind"] == "循环里面指定方法" else "use the published metadata/cache API; never mutate MetadataDao internals"))
|
|
648
|
+
for offset, literal in src.literals:
|
|
649
|
+
if "/*dialect*/" in literal.lower():
|
|
650
|
+
issues.append(_issue(src, offset, "CVE-KD-0060-1", "dialect SQL in Java source", "use QueryServiceHelper/QFilter instead of dialect SQL"))
|
|
651
|
+
return issues
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _additional_security_issues(src: _Source) -> list[dict]:
|
|
655
|
+
issues = []
|
|
656
|
+
patterns = (
|
|
657
|
+
(r"\bRuntime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*exec\s*\(|\bnew\s+(?:java\.lang\.)?ProcessBuilder\s*\(",
|
|
658
|
+
"Command Injection", "external process execution", "use platform services; never build shell commands from request values"),
|
|
659
|
+
(r"\bScriptEngineManager\b|\.\s*eval\s*\(", "Dynamic Code Evaluation: Script Injection", "dynamic script evaluation", "replace evaluation with explicit Java logic and validated parameters"),
|
|
660
|
+
)
|
|
661
|
+
for pattern, name, message, fix in patterns:
|
|
662
|
+
codes = [code for code, entry in CATALOG["rules"].items() if entry["engine"] == "fortify" and entry["name"] == name]
|
|
663
|
+
for match in re.finditer(pattern, src.code):
|
|
664
|
+
# 不知道具体 Fortify 调用链,不把危险入口直接当作已证实注入。
|
|
665
|
+
issues.append(_issue(src, match.start(), codes, message + "; verify input provenance", fix, ADVISE))
|
|
666
|
+
predictable_seed = re.compile(r"[-+]?\d+[lL]?|new\s+byte\s*\[\s*\d+\s*\]|new\s+byte\s*\[\s*\]\s*\{\s*[-+\d,\s]*\}")
|
|
667
|
+
for created in re.finditer(r"\bnew\s+(?:java\.security\.)?SecureRandom\s*\(", src.code):
|
|
668
|
+
arguments = _arguments(src, created.end() - 1)
|
|
669
|
+
if arguments and (predictable_seed.fullmatch(arguments[0]) or "currentTimeMillis" in arguments[0]):
|
|
670
|
+
issues.append(_issue(src, created.start(), "java:S4347", "predictable SecureRandom seed", "use new SecureRandom() without a manually predictable seed"))
|
|
671
|
+
for call in _instance_calls(src, created):
|
|
672
|
+
arguments = _arguments(src, call.end() - 1)
|
|
673
|
+
if call.group(1) == "setSeed" and arguments and (predictable_seed.fullmatch(arguments[0]) or "currentTimeMillis" in arguments[0]):
|
|
674
|
+
issues.append(_issue(src, call.start(), "java:S4347", "predictable SecureRandom seed", "allow SecureRandom to seed itself"))
|
|
675
|
+
for name, pattern, fix in (
|
|
676
|
+
("Server-Side Request Forgery", r"\bnew\s+(?:java\.net\.)?URL\s*\(", "allowlist protocol and destination host; reject private-network addresses and revalidate redirects"),
|
|
677
|
+
("Open Redirect", r"\.\s*sendRedirect\s*\(", "use a trusted relative path or an explicit destination allowlist"),
|
|
678
|
+
("Header Manipulation", r"\.\s*(?:setHeader|addHeader)\s*\(", "reject CR/LF and validate header names/values"),
|
|
679
|
+
("Path Manipulation", r"\bnew\s+(?:java\.io\.)?File\s*\(|\bPaths\s*\.\s*get\s*\(", "resolve under a trusted root and check normalized containment before filesystem access"),
|
|
680
|
+
):
|
|
681
|
+
codes = [code for code, entry in CATALOG["rules"].items() if entry["engine"] == "fortify" and entry["name"] == name]
|
|
682
|
+
for match in re.finditer(pattern, src.code):
|
|
683
|
+
arguments = _arguments(src, match.end() - 1)
|
|
684
|
+
if not arguments or all(re.fullmatch(r'"(?:\\.|[^"\\])*"', argument) for argument in arguments):
|
|
685
|
+
continue
|
|
686
|
+
issues.append(_issue(src, match.start(), codes, name + ": dynamic input needs data-flow review", fix, ADVISE))
|
|
687
|
+
for match in re.finditer(r"\.\s*(?:enableDefaultTyping|activateDefaultTyping)\s*\(", src.code):
|
|
688
|
+
issues.append(_issue(src, match.start(), ("java:S4544", "FORTIFY:Unsafe Deserialization"), "Jackson polymorphic deserialization needs validation",
|
|
689
|
+
"deserialize into explicit DTO types; do not enable unrestricted default typing", ADVISE))
|
|
690
|
+
for call in _receiver_calls(src, r"HttpsURLConnection|javax\.net\.ssl\.HttpsURLConnection"):
|
|
691
|
+
if call.group(1) not in ("setHostnameVerifier", "setDefaultHostnameVerifier"):
|
|
692
|
+
continue
|
|
693
|
+
arguments = _arguments(src, call.end() - 1)
|
|
694
|
+
if arguments and re.fullmatch(r"\([^()]*\)\s*->\s*(?:true|\{\s*return\s+true\s*;\s*\})", arguments[0]):
|
|
695
|
+
issues.append(_issue(src, call.start(), "java:S5527", "hostname verifier accepts every host", "use the default hostname verifier; never bypass TLS identity checks"))
|
|
696
|
+
# 定位具体 TLS 方法声明,兼顾通用方法读取器未枚举的局部类和匿名类。
|
|
697
|
+
for method in re.finditer(r"\b(void|boolean)\s+(checkServerTrusted|checkClientTrusted|verify)\s*\(", src.code):
|
|
698
|
+
params_close = src.pair.get(method.end() - 1)
|
|
699
|
+
if params_close is None:
|
|
700
|
+
continue
|
|
701
|
+
tail = re.match(r"\s*(?:throws\s+[\w$.,\s]+)?\{", src.code[params_close + 1:])
|
|
702
|
+
if tail is None:
|
|
703
|
+
continue
|
|
704
|
+
body_start = params_close + 1 + tail.end()
|
|
705
|
+
body_close = src.pair.get(body_start - 1)
|
|
706
|
+
if body_close is None:
|
|
707
|
+
continue
|
|
708
|
+
body = src.code[body_start:body_close].strip()
|
|
709
|
+
ancestors = [opening for opening, close in src.pair.items() if src.code[opening] == "{" and opening < method.start() < close]
|
|
710
|
+
owner_start = max(ancestors, default=0)
|
|
711
|
+
owner_head = src.code[max(src.code.rfind(char, 0, owner_start) for char in ";{}") + 1:owner_start]
|
|
712
|
+
if method.group(1) == "void" and method.group(2) in ("checkServerTrusted", "checkClientTrusted") and re.search(r"\b(?:X509TrustManager|X509ExtendedTrustManager)\b", owner_head) and not body:
|
|
713
|
+
issues.append(_issue(src, method.start(2), "java:S4830", "trust manager accepts certificates without validation", "use the platform/default trust manager and trusted certificate chain"))
|
|
714
|
+
if method.group(1) == "boolean" and method.group(2) == "verify" and re.search(r"\bHostnameVerifier\b", owner_head) and re.fullmatch(r"return\s+true\s*;", body):
|
|
715
|
+
issues.append(_issue(src, method.start(2), "java:S5527", "hostname verifier always returns true", "use the default hostname verifier"))
|
|
716
|
+
return issues
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _nested_function(src: _Source, offset: int, boundary: int) -> bool:
|
|
720
|
+
"""判断跳转是否属于指定边界内的另一函数或类声明。"""
|
|
721
|
+
if any(method["headStart"] >= boundary and method["start"] <= offset <= method["end"] for method in src.methods):
|
|
722
|
+
return True
|
|
723
|
+
nested = [opening for opening, end in src.pair.items() if src.code[opening] == "{" and boundary <= opening < offset < end]
|
|
724
|
+
if any(_block_owner(src, opening) == "->" for opening in nested):
|
|
725
|
+
return True
|
|
726
|
+
# 通用方法读取器只枚举外层方法,因此还需排除局部类和匿名类的整个声明体。
|
|
727
|
+
return any(re.search(r"\b(?:class|interface|enum|record)\s+\w+|\bnew\s+[\w$.]+(?:<[^;(){}]*>)?\s*\([^;{}]*\)\s*$",
|
|
728
|
+
src.code[max(src.code.rfind(char, 0, opening) for char in ";{}") + 1:opening]) for opening in nested)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _finally_jumps(src: _Source):
|
|
732
|
+
"""仅识别会离开 finally 的跳转,不把嵌套函数或内部循环的跳转当作退出。"""
|
|
733
|
+
for match in re.finditer(r"\bfinally\s*\{", src.code):
|
|
734
|
+
close = src.pair.get(match.end() - 1)
|
|
735
|
+
if close is None:
|
|
736
|
+
continue
|
|
737
|
+
for jump in re.finditer(r"\b(return|break|continue|throw)\b", src.code[match.end():close]):
|
|
738
|
+
offset = match.end() + jump.start()
|
|
739
|
+
nested = [(opening, end) for opening, end in src.pair.items()
|
|
740
|
+
if src.code[opening] == "{" and match.end() <= opening < offset < end]
|
|
741
|
+
if _nested_function(src, offset, match.end()):
|
|
742
|
+
continue
|
|
743
|
+
if jump.group(1) in ("break", "continue"):
|
|
744
|
+
# 带标签跳转需要解析目标;源码预检不猜测其目标是否位于 finally 外。
|
|
745
|
+
if re.match(r"\s+[A-Za-z_$]", src.code[offset + len(jump.group(1)):]):
|
|
746
|
+
continue
|
|
747
|
+
if any(match.end() <= start < offset < end for start, end in src.loops):
|
|
748
|
+
continue
|
|
749
|
+
if jump.group(1) == "break" and any(_block_owner(src, opening) == "switch" for opening, _end in nested):
|
|
750
|
+
continue
|
|
751
|
+
caught = jump.group(1) == "throw" and any(
|
|
752
|
+
_block_owner(src, opening) == "try" and re.match(r"\s*catch\b", src.code[end + 1:])
|
|
753
|
+
for opening, end in nested
|
|
754
|
+
)
|
|
755
|
+
yield offset, jump.group(1), ADVISE if caught else BLOCK
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _additional_code_issues(src: _Source) -> list[dict]:
|
|
759
|
+
issues = []
|
|
760
|
+
for method in src.methods:
|
|
761
|
+
body = src.code[method["start"]:method["end"]]
|
|
762
|
+
if method["name"] == "finalize" and not method["params"].strip():
|
|
763
|
+
issues.append(_issue(src, method["nameOffset"], "java:S1113", "finalize() override", "remove finalization; manage resources explicitly with try-with-resources"))
|
|
764
|
+
if re.search(r"\bBoolean\s*$", method["prefix"]):
|
|
765
|
+
for match in re.finditer(r"\breturn\s+null\s*;", body):
|
|
766
|
+
offset = method["start"] + match.start()
|
|
767
|
+
if _nested_function(src, offset, method["start"]):
|
|
768
|
+
continue
|
|
769
|
+
issues.append(_issue(src, offset, "java:S2447", "Boolean method returns null", "return Boolean.TRUE/FALSE or use a primitive boolean"))
|
|
770
|
+
break
|
|
771
|
+
resources = [(method["start"] + match.end() - 1, src.pair.get(method["start"] + match.end() - 1, -1)) for match in re.finditer(r"\btry\s*\(", body)]
|
|
772
|
+
for created in re.finditer(r"\b(?:([\w$]+)\s*=\s*)new\s+(?:[\w$]+\.)*(FileInputStream|FileOutputStream|FileReader|FileWriter|BufferedReader|BufferedWriter|DataInputStream|DataOutputStream|ZipFile)\s*\(", body):
|
|
773
|
+
offset = method["start"] + created.start()
|
|
774
|
+
if any(opening < offset < close for opening, close in resources):
|
|
775
|
+
continue
|
|
776
|
+
after = body[created.end():]
|
|
777
|
+
if re.search(rf"\b{re.escape(created.group(1))}\s*\.\s*close\s*\(", after):
|
|
778
|
+
continue
|
|
779
|
+
issues.append(_issue(src, offset, ("java:S2095", "java:S2093"), "resource closure/ownership is not verified",
|
|
780
|
+
"use try-with-resources; if ownership is transferred, verify the caller closes it", ADVISE))
|
|
781
|
+
for offset, jump, level in _finally_jumps(src):
|
|
782
|
+
rule = "java:S1163" if jump == "throw" else "java:S1143"
|
|
783
|
+
issues.append(_issue(src, offset, rule, f"{jump} inside finally may discard the original outcome", "move control-flow changes outside finally", level))
|
|
784
|
+
for call in _receiver_calls(src, "Thread|java\\.lang\\.Thread"):
|
|
785
|
+
if call.group(1) in ("wait", "notify", "notifyAll"):
|
|
786
|
+
issues.append(_issue(src, call.start(), "java:S2236", "monitor operation on a Thread instance", "use a dedicated lock object or platform synchronization API"))
|
|
787
|
+
jdbc_values = "Array|AsciiStream|BigDecimal|BinaryStream|Blob|Boolean|Byte|Bytes|CharacterStream|Clob|Date|Double|Float|Int|Long|NCharacterStream|NClob|NString|Null|Object|Ref|RowId|Short|SQLXML|String|Time|Timestamp|UnicodeStream|URL"
|
|
788
|
+
for types, methods in ((r"PreparedStatement|CallableStatement|java\.sql\.PreparedStatement|java\.sql\.CallableStatement", rf"set(?:{jdbc_values})|registerOutParameter"),
|
|
789
|
+
(r"ResultSet|java\.sql\.ResultSet", rf"(?:get|update)(?:{jdbc_values})")):
|
|
790
|
+
for call in _receiver_calls(src, types):
|
|
791
|
+
if not re.fullmatch(methods, call.group(1)):
|
|
792
|
+
continue
|
|
793
|
+
arguments = _arguments(src, call.end() - 1)
|
|
794
|
+
if arguments and re.fullmatch(r"[-+]?\d+[lL]?", arguments[0]) and int(arguments[0].rstrip("lL")) <= 0:
|
|
795
|
+
issues.append(_issue(src, call.start(), "java:S2695", "JDBC index is not positive (indices start at 1)", "use a valid 1-based parameter/column index"))
|
|
796
|
+
for match in re.finditer(r"\b(?:System|Runtime)\s*\.\s*runFinalizersOnExit\s*\(", src.code):
|
|
797
|
+
issues.append(_issue(src, match.start(), "java:S2151", "runFinalizersOnExit is unsafe", "remove the call"))
|
|
798
|
+
for match in re.finditer(r"\b(?:new\s+(?:java\.lang\.)?ThreadGroup\s*\(|(?:java\.lang\.)?ThreadGroup\s+\w+)", src.code):
|
|
799
|
+
issues.append(_issue(src, match.start(), "java:S3014", "ThreadGroup use", "use kd.bos.threads.ThreadPools"))
|
|
800
|
+
for match in re.finditer(r"\.\s*indexOf\s*\([^;()]*\)\s*>\s*0\b", src.code):
|
|
801
|
+
issues.append(_issue(src, match.start(), "java:S2692", "indexOf > 0 excludes a match at the start", "use >= 0 when checking containment, or justify excluding index 0", ADVISE))
|
|
802
|
+
return issues
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
# --- Static collections ---------------------------------------------------------------------
|
|
806
|
+
|
|
807
|
+
_STATIC_FIELD_RE = re.compile(
|
|
808
|
+
r"\bstatic\s+(?:(?:final|transient|volatile|public|protected|private)\s+)*"
|
|
809
|
+
r"(?:[\w$]+\.)*(?P<type>[A-Za-z_$][\w$]*)\s*(?:<[^;=(){}]*>)?\s*(?:\[\s*\])*\s+"
|
|
810
|
+
r"(?P<name>[A-Za-z_$][\w$]*)\s*(?P<end>[=;,])"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _static_collection_issues(src: _Source) -> list[dict]:
|
|
815
|
+
issues = []
|
|
816
|
+
for match in _STATIC_FIELD_RE.finditer(src.code):
|
|
817
|
+
declared = match.group("type")
|
|
818
|
+
if declared not in _STATIC_COLLECTIONS or re.search(r"\[\s*\]", match.group(0)):
|
|
819
|
+
continue
|
|
820
|
+
codes = [f"CVE-KD-0056-{_STATIC_COLLECTIONS[declared]}"]
|
|
821
|
+
if match.group("end") == "=":
|
|
822
|
+
created = re.match(r"\s*new\s+(?:[\w$]+\.)*([A-Za-z_$][\w$]*)", src.code[match.end():])
|
|
823
|
+
if created and created.group(1) in _STATIC_COLLECTIONS and created.group(1) != declared:
|
|
824
|
+
codes.append(f"CVE-KD-0056-{_STATIC_COLLECTIONS[created.group(1)]}")
|
|
825
|
+
issues.append(_issue(
|
|
826
|
+
src, match.start(), codes,
|
|
827
|
+
f"static collection field '{match.group('name')}' ({declared}); DCS rejects static collections even when final",
|
|
828
|
+
"keep the collection in a local variable or an instance field; for fixed lookup values use a "
|
|
829
|
+
"static final array, a switch or an enum",
|
|
830
|
+
))
|
|
831
|
+
return issues
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# --- Calls inside loops ---------------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
_DB_HELPERS = "BusinessDataServiceHelper|QueryServiceHelper|SaveServiceHelper|DeleteServiceHelper|OperationServiceHelper|BusinessDataReader|BusinessDataWriter|DB|ORM|DBServiceHelper"
|
|
837
|
+
_LOOP_SERVICES = {
|
|
838
|
+
"RedisSessionlessCache": "CVE-KD-0051-03", "DistributeSessionlessCache": "CVE-KD-0051-03",
|
|
839
|
+
"RedisSessionableCache": "CVE-KD-0051-04", "DistributeSessionableCache": "CVE-KD-0051-04",
|
|
840
|
+
"BusinessDataReader": "CVE-KD-0051-07", "BusinessDataWriter": "CVE-KD-0051-08",
|
|
841
|
+
"OperationServiceHelper": "CVE-KD-0051-09", "SaveServiceHelper": "CVE-KD-0051-10",
|
|
842
|
+
"DeleteServiceHelper": "CVE-KD-0051-11",
|
|
843
|
+
}
|
|
844
|
+
_LOOP_FIX = "load or query everything once before the loop (one call with an IN filter / id array), work on the result in memory, and save once after the loop"
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def _loop_issues(src: _Source) -> list[dict]:
|
|
848
|
+
issues: list[dict] = []
|
|
849
|
+
seen: set[int] = set()
|
|
850
|
+
|
|
851
|
+
def add(offset: int, codes, message: str, fix: str, level: str = BLOCK) -> None:
|
|
852
|
+
if offset not in seen:
|
|
853
|
+
seen.add(offset)
|
|
854
|
+
issues.append(_issue(src, offset, codes, message, fix, level))
|
|
855
|
+
|
|
856
|
+
# Database and service access, with the nesting depth that decides 1215-1 / 1215-2.
|
|
857
|
+
orm_names = _typed_names(src, "ORM")
|
|
858
|
+
db_receivers = _DB_HELPERS + ("|" + "|".join(re.escape(name) for name in sorted(orm_names)) if orm_names else "")
|
|
859
|
+
for match in re.finditer(rf"(?<![\w$.])({db_receivers})\s*\.\s*([A-Za-z_$][\w$]*)\s*\(", src.code):
|
|
860
|
+
depth = src.loop_depth(match.start())
|
|
861
|
+
if not depth:
|
|
862
|
+
continue
|
|
863
|
+
receiver, method = match.group(1), match.group(2)
|
|
864
|
+
codes = []
|
|
865
|
+
if receiver in _LOOP_SERVICES:
|
|
866
|
+
codes.append(_LOOP_SERVICES[receiver])
|
|
867
|
+
if receiver == "BusinessDataServiceHelper" and method == "loadHead":
|
|
868
|
+
codes.append("CVE-KD-TS-0051-46")
|
|
869
|
+
if depth >= 3:
|
|
870
|
+
codes.append("CVE-KD-1215-2")
|
|
871
|
+
elif depth == 2:
|
|
872
|
+
codes.append("CVE-KD-1215-1")
|
|
873
|
+
call = f"{receiver}.{method}"
|
|
874
|
+
if codes:
|
|
875
|
+
add(match.start(), codes, f"{call} inside {depth} nested loop(s)", _LOOP_FIX)
|
|
876
|
+
else:
|
|
877
|
+
add(match.start(), "CVE-KD-1215-1", f"{call} inside a loop reads/writes the database once per row; "
|
|
878
|
+
"platform standards forbid it and DCS rejects it once loops nest", _LOOP_FIX, ADVISE)
|
|
879
|
+
for types, code in (("RedisSessionlessCache|DistributeSessionlessCache", "CVE-KD-0051-03"),
|
|
880
|
+
("RedisSessionableCache|DistributeSessionableCache", "CVE-KD-0051-04")):
|
|
881
|
+
for name in _typed_names(src, types):
|
|
882
|
+
for match in re.finditer(rf"(?<![\w$.]){re.escape(name)}\s*\.\s*\w+\s*\(", src.code):
|
|
883
|
+
if src.loop_depth(match.start()):
|
|
884
|
+
add(match.start(), code, "distributed cache access inside a loop", "read/write the cache in one batch call outside the loop")
|
|
885
|
+
for match in re.finditer(r"\bupdateView\s*\(", src.code):
|
|
886
|
+
if src.loop_depth(match.start()):
|
|
887
|
+
add(match.start(), "CVE-KD-0052", "updateView() inside a loop", "set values in the loop and call getView().updateView(key) once after it")
|
|
888
|
+
page_cache = _typed_names(src, "IPageCache|PageCache|RootPageCache")
|
|
889
|
+
cache_calls = r"\bget(?:Root)?PageCache\s*\(\s*\)\s*\.\s*\w+\s*\("
|
|
890
|
+
if page_cache:
|
|
891
|
+
cache_calls += "|" + "|".join(rf"(?<![\w$.]){re.escape(name)}\s*\.\s*\w+\s*\(" for name in sorted(page_cache))
|
|
892
|
+
for match in re.finditer(cache_calls, src.code):
|
|
893
|
+
if src.loop_depth(match.start()):
|
|
894
|
+
code = "CVE-KD-0054-2" if "Root" in match.group(0) else "CVE-KD-0054-1"
|
|
895
|
+
add(match.start(), code, "page cache access inside a loop", "collect the values in a local map and put/read the page cache once outside the loop")
|
|
896
|
+
log_names = _typed_names(src, "Log|Logger") | {"log", "logger", "LOG", "LOGGER"}
|
|
897
|
+
log_pattern = rf"(?<![\w$.])(?:{'|'.join(re.escape(name) for name in sorted(log_names))}|LogFactory\s*\.\s*getLog\s*\([^)]*\))\s*\.\s*(?:trace|debug|info|warn|error|fatal)\s*\("
|
|
898
|
+
for match in re.finditer(log_pattern, src.code):
|
|
899
|
+
if src.loop_depth(match.start()):
|
|
900
|
+
add(match.start(), ("CVE-KD-0058-1",), "log call inside a loop",
|
|
901
|
+
"collect what matters in the loop and write one summary log after it")
|
|
902
|
+
for match in re.finditer(r"\bDispatchServiceHelper\s*\.\s*invoke\w*\s*\(|\bServiceFactory\s*\.\s*getService\s*\([^)]*\)\s*\.\s*\w+\s*\(", src.code):
|
|
903
|
+
if src.loop_depth(match.start()):
|
|
904
|
+
add(match.start(), "CVE-KD-0201", "microservice call inside a loop", "pass the whole batch to one service call outside the loop")
|
|
905
|
+
for match in re.finditer(r"\bPattern\s*\.\s*compile\s*\(", src.code):
|
|
906
|
+
if src.loop_depth(match.start()):
|
|
907
|
+
add(match.start(), "IIL_PATTERN_COMPILE_IN_LOOP", "Pattern.compile inside a loop", "compile once into a private static final Pattern")
|
|
908
|
+
for method in src.methods:
|
|
909
|
+
body = src.code[method["start"]:method["end"]]
|
|
910
|
+
strings = set(re.findall(r"\bString\s+([A-Za-z_$][\w$]*)\s*[=;]", body))
|
|
911
|
+
for name in strings:
|
|
912
|
+
for match in re.finditer(rf"(?<![\w$.]){re.escape(name)}\s*(?:\+=|=\s*{re.escape(name)}\s*\+)", body):
|
|
913
|
+
offset = method["start"] + match.start()
|
|
914
|
+
if src.loop_depth(offset):
|
|
915
|
+
add(offset, "java:S1643", f"string '{name}' concatenated with + inside a loop", "append to a StringBuilder and convert once after the loop")
|
|
916
|
+
return issues
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
# --- Data access: filters, deletes, SQL keywords ---------------------------------------------
|
|
920
|
+
|
|
921
|
+
# SDK 重载签名;None 表示按主键读取,不得当作无条件查询。
|
|
922
|
+
_FILTER_OVERLOADS = {
|
|
923
|
+
("QueryServiceHelper", "query"): (
|
|
924
|
+
(("String", "String", "QFilter[]"), 2),
|
|
925
|
+
(("String", "String", "QFilter[]", "String"), 2),
|
|
926
|
+
(("String", "String", "QFilter[]", "String", "int"), 2),
|
|
927
|
+
(("String", "String", "String", "QFilter[]", "String"), 3),
|
|
928
|
+
(("String", "String", "String", "QFilter[]", "String", "int"), 3),
|
|
929
|
+
),
|
|
930
|
+
("QueryServiceHelper", "queryOne"): ((("String", "String", "QFilter[]"), 2),),
|
|
931
|
+
("QueryServiceHelper", "queryDataSet"): (
|
|
932
|
+
(("String", "String", "String", "QFilter[]", "String"), 3),
|
|
933
|
+
(("String", "String", "String", "QFilter[]", "String", "int"), 3),
|
|
934
|
+
),
|
|
935
|
+
("QueryServiceHelper", "exists"): ((("String", "Object"), None), (("String", "QFilter[]"), 1)),
|
|
936
|
+
("QueryServiceHelper", "queryPrimaryKeys"): (
|
|
937
|
+
(("String", "QFilter[]", "String", "int"), 1),
|
|
938
|
+
(("String", "String", "QFilter[]", "String", "int"), 2),
|
|
939
|
+
(("String", "IDataEntityType", "QFilter[]", "String", "int"), 2),
|
|
940
|
+
),
|
|
941
|
+
("BusinessDataServiceHelper", "load"): (
|
|
942
|
+
(("String", "String", "QFilter[]"), 2),
|
|
943
|
+
(("String", "String", "QFilter[]", "String"), 2),
|
|
944
|
+
(("String", "String", "QFilter[]", "String", "int"), 2),
|
|
945
|
+
(("String", "String", "QFilter[]", "String", "int", "int"), 2),
|
|
946
|
+
(("Object[]", "DynamicObjectType"), None),
|
|
947
|
+
),
|
|
948
|
+
("BusinessDataServiceHelper", "loadFromCache"): (
|
|
949
|
+
(("Object[]", "DynamicObjectType"), None),
|
|
950
|
+
(("String", "String", "QFilter[]"), 2),
|
|
951
|
+
(("String", "String", "QFilter[]", "String"), 2),
|
|
952
|
+
(("DynamicObjectType", "QFilter[]"), 1),
|
|
953
|
+
),
|
|
954
|
+
("BusinessDataServiceHelper", "loadSingle"): (
|
|
955
|
+
(("Object", "DynamicObjectType"), None),
|
|
956
|
+
(("Object", "DynamicObjectType", "int", "int"), None),
|
|
957
|
+
(("Object", "String"), None),
|
|
958
|
+
(("Object", "String", "String"), None),
|
|
959
|
+
(("String", "QFilter[]"), 1),
|
|
960
|
+
(("String", "String", "QFilter[]"), 2),
|
|
961
|
+
),
|
|
962
|
+
("BusinessDataServiceHelper", "loadSingleFromCache"): (
|
|
963
|
+
(("Object", "String", "String"), None),
|
|
964
|
+
(("String", "String", "QFilter[]"), 2),
|
|
965
|
+
(("Object", "DynamicObjectType"), None),
|
|
966
|
+
),
|
|
967
|
+
}
|
|
968
|
+
_EMPTY_FILTER_RE = re.compile(r"^(?:\(\s*(?:kd\.bos\.orm\.query\.)?QFilter\s*\[\s*\]\s*\)\s*)?null$|^new\s+(?:kd\.bos\.orm\.query\.)?QFilter\s*\[\s*0?\s*\]\s*(?:\{\s*\})?$")
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _arguments(src: _Source, open_paren: int) -> list[str]:
|
|
972
|
+
"""The call's arguments, split on the code view (commas in literals do not count) and read from
|
|
973
|
+
the literal-preserving view so a string argument still holds a position."""
|
|
974
|
+
close = src.pair.get(open_paren)
|
|
975
|
+
if close is None:
|
|
976
|
+
return []
|
|
977
|
+
parts, start, index = [], open_paren + 1, open_paren + 1
|
|
978
|
+
while index < close:
|
|
979
|
+
char = src.code[index]
|
|
980
|
+
if char in "({[" and index in src.pair:
|
|
981
|
+
index = src.pair[index] + 1
|
|
982
|
+
continue
|
|
983
|
+
if char == ",":
|
|
984
|
+
parts.append(src.lit[start:index])
|
|
985
|
+
start = index + 1
|
|
986
|
+
index += 1
|
|
987
|
+
parts.append(src.lit[start:close])
|
|
988
|
+
arguments = [" ".join(part.split()) for part in parts]
|
|
989
|
+
return arguments if any(arguments) else []
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _expression_type(src: _Source, expression: str, offset: int) -> str | None:
|
|
993
|
+
"""仅推断字面量、显式类型和当前方法中的声明;未知表达式不冒充已解析重载。"""
|
|
994
|
+
if expression == "null":
|
|
995
|
+
return "null"
|
|
996
|
+
if re.fullmatch(r'"(?:\\.|[^"\\])*"', expression):
|
|
997
|
+
return "String"
|
|
998
|
+
if re.fullmatch(r"[-+]?\d+|(?:Integer\.)?(?:MAX_VALUE|MIN_VALUE)", expression):
|
|
999
|
+
return "int"
|
|
1000
|
+
if re.fullmatch(r"[-+]?\d+[lL]", expression):
|
|
1001
|
+
return "long"
|
|
1002
|
+
typed = re.match(r"\(\s*([\w$.]+)\s*(\[\s*\])?\s*\)", expression) or re.match(r"new\s+([\w$.]+)\s*(\[)?", expression)
|
|
1003
|
+
if typed:
|
|
1004
|
+
return typed.group(1).rsplit(".", 1)[-1] + ("[]" if typed.group(2) else "")
|
|
1005
|
+
if re.fullmatch(r"(?:this\.)?[A-Za-z_$][\w$]*", expression):
|
|
1006
|
+
name = expression.removeprefix("this.")
|
|
1007
|
+
method = src.method_at(offset)
|
|
1008
|
+
scope = src.code[method["headStart"]:offset] if method else src.code[:offset]
|
|
1009
|
+
declarations = list(re.finditer(rf"\b([\w$.]+)\s*(\[\s*\])?\s+{re.escape(name)}\s*(?=[=;,)]|$)", scope))
|
|
1010
|
+
if declarations and declarations[-1].group(1) not in _NOT_TYPES and not expression.startswith("this."):
|
|
1011
|
+
declared = declarations[-1]
|
|
1012
|
+
return declared.group(1).rsplit(".", 1)[-1] + ("[]" if declared.group(2) else "")
|
|
1013
|
+
field = next((item for item in _fields(src) if item["name"] == name), None)
|
|
1014
|
+
if field:
|
|
1015
|
+
return field["type"].rsplit(".", 1)[-1]
|
|
1016
|
+
return None
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _data_access_issues(src: _Source) -> list[dict]:
|
|
1020
|
+
issues = []
|
|
1021
|
+
for match in re.finditer(r"(?<![\w$.])(QueryServiceHelper|BusinessDataServiceHelper)\s*\.\s*(\w+)\s*\(", src.code):
|
|
1022
|
+
overloads = _FILTER_OVERLOADS.get((match.group(1), match.group(2)), ())
|
|
1023
|
+
arguments = _arguments(src, match.end() - 1)
|
|
1024
|
+
types = [_expression_type(src, argument, match.start()) for argument in arguments]
|
|
1025
|
+
candidates = [(signature, index) for signature, index in overloads if len(signature) == len(types) and all(
|
|
1026
|
+
actual is None or actual == expected or expected == "Object"
|
|
1027
|
+
or (actual == "null" and expected != "int")
|
|
1028
|
+
for actual, expected in zip(types, signature))]
|
|
1029
|
+
# exists(entity, null) 按 Java 的最具体重载选择 QFilter[],显式 Object 转型则是主键。
|
|
1030
|
+
if match.group(1, 2) == ("QueryServiceHelper", "exists") and len(types) == 2 and types[1] in ("null", "QFilter[]"):
|
|
1031
|
+
candidates = [item for item in candidates if item[1] is not None]
|
|
1032
|
+
empty = [index for _signature, index in candidates if index is not None and _EMPTY_FILTER_RE.fullmatch(arguments[index])]
|
|
1033
|
+
if empty:
|
|
1034
|
+
proven = len(empty) == len(candidates)
|
|
1035
|
+
issues.append(_issue(
|
|
1036
|
+
src, match.start(), "CVE-KD-0303",
|
|
1037
|
+
f"{match.group(1)}.{match.group(2)} " + ("without a filter reads the whole table" if proven else "may select an overload with an empty filter; verify the argument types"),
|
|
1038
|
+
"pass a real QFilter (id IN, bill number, organization, date range ...); never null or an empty QFilter array",
|
|
1039
|
+
BLOCK if proven else ADVISE,
|
|
1040
|
+
))
|
|
1041
|
+
sql_rules = (
|
|
1042
|
+
(r"\bcreate\s+view\b", "CVE-KD-0720-1", "create view in code", "model an entity/report instead of a database view"),
|
|
1043
|
+
(r"\bselect\s+\*", "CVE-KD-0053-01", "select * in SQL", "list the needed columns explicitly"),
|
|
1044
|
+
(r"\bdrop\s+table\b", "CVE-KD-0013-1", "drop table in code", "schema changes are not allowed from plugin code"),
|
|
1045
|
+
(r"\btruncate\s+table\b", "CVE-KD-0013-2", "truncate table in code", "schema/data wipes are not allowed from plugin code"),
|
|
1046
|
+
(r"\brename\s+table\b|\balter\s+table\s+\S+\s+rename\b", "CVE-KD-0013-3", "rename table in code", "schema changes are not allowed from plugin code"),
|
|
1047
|
+
(r"\bcreate\s+(?:or\s+replace\s+)?view\b", "CVE-KD-0720-2", "create view in code", "model the data as an entity/report instead of a database view"),
|
|
1048
|
+
(r"\bcreate\s+or\s+replace\s+procedure\b", "CVE-KD-0721-2", "create procedure in code", "implement the logic in Java"),
|
|
1049
|
+
(r"\bcreate\s+procedure\b", "CVE-KD-0721-1", "create procedure in code", "implement the logic in Java"),
|
|
1050
|
+
(r"\bcreate\s+or\s+replace\s+function\b", "CVE-KD-0722-2", "create function in code", "implement the logic in Java"),
|
|
1051
|
+
(r"\bcreate\s+function\b", "CVE-KD-0722-1", "create function in code", "implement the logic in Java"),
|
|
1052
|
+
(r"\bcreate\s+or\s+replace\s+trigger\b", "CVE-KD-0723-2", "create trigger in code", "use an operation plugin instead of a trigger"),
|
|
1053
|
+
(r"\bcreate\s+trigger\b", "CVE-KD-0723-1", "create trigger in code", "use an operation plugin instead of a trigger"),
|
|
1054
|
+
)
|
|
1055
|
+
for offset, text in src.literals:
|
|
1056
|
+
for pattern, code, what, fix in sql_rules:
|
|
1057
|
+
if re.search(pattern, text, re.IGNORECASE):
|
|
1058
|
+
issues.append(_issue(src, offset, code, f"{what} is forbidden by DCS", fix))
|
|
1059
|
+
if re.search(r"\bdelete\s+from\b", text, re.IGNORECASE):
|
|
1060
|
+
statement = src.lit[offset:src.statement_end(offset)]
|
|
1061
|
+
if not re.search(r"\bwhere\b", statement, re.IGNORECASE):
|
|
1062
|
+
issues.append(_issue(src, offset, "CVE-KD-0302-01", "delete without a WHERE condition",
|
|
1063
|
+
"delete through OperationServiceHelper.executeOperate(\"delete\", ...) with explicit ids"))
|
|
1064
|
+
for token, number in _FILE_SERVICE_TOKENS.items():
|
|
1065
|
+
if token.lower() in text.lower():
|
|
1066
|
+
issues.append(_issue(src, offset, f"CVE-KD-0026-{number}", f"file-service internal keyword '{token}'",
|
|
1067
|
+
"use the attachment/file service helpers (AttachmentServiceHelper, FileServiceFactory) instead of internal URLs"))
|
|
1068
|
+
for token, number in _TEMPFILE_TOKENS.items():
|
|
1069
|
+
if token.lower() in text.lower():
|
|
1070
|
+
issues.append(_issue(src, offset, f"CVE-KD-0028-{number}", f"temporary-file internal keyword '{token}'",
|
|
1071
|
+
"use the documented attachment APIs; do not inspect the temporary file storage configuration"))
|
|
1072
|
+
break
|
|
1073
|
+
for match in re.finditer(r"\"[^\"\n]*\b(?:select|update|delete|insert)\b[^\"\n]*\"\s*\+\s*[A-Za-z_$(]", src.lit, re.IGNORECASE):
|
|
1074
|
+
if re.search(r"\b(?:select\s+.+\bfrom|update\s+\w+\s+set|delete\s+from|insert\s+into)\b", match.group(0), re.IGNORECASE):
|
|
1075
|
+
issues.append(_issue(src, match.start(), ("FORTIFY:SQL Injection", "java:S2077"), "SQL built by string concatenation",
|
|
1076
|
+
"query through QFilter with QueryServiceHelper; never concatenate values into SQL", ADVISE))
|
|
1077
|
+
return issues
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
# --- Suppression, identifiers, packaging ------------------------------------------------------
|
|
1081
|
+
|
|
1082
|
+
def _suppression_and_naming_issues(src: _Source) -> list[dict]:
|
|
1083
|
+
issues = []
|
|
1084
|
+
for match in re.finditer(r"NOSONAR", src.raw, re.IGNORECASE):
|
|
1085
|
+
if src.code[match.start()] == " ":
|
|
1086
|
+
issues.append(_issue(src, match.start(), "CVE-KD-0401", "NOSONAR comment suppresses the DCS scan", "remove the comment and fix the reported problem"))
|
|
1087
|
+
for match in re.finditer(r"@SuppressWarnings\s*\(([^)]*)\)", src.lit):
|
|
1088
|
+
if re.search(r"squid:|java:S|\"S\d+", match.group(1)):
|
|
1089
|
+
issues.append(_issue(src, match.start(), "CVE-KD-0402", "@SuppressWarnings with a Sonar rule suppresses the DCS scan", "remove the suppression and fix the reported problem"))
|
|
1090
|
+
for match in re.finditer(r"[\u3400-\u9fff\uf900-\ufaff]+", src.code):
|
|
1091
|
+
issues.append(_issue(src, match.start(), "CVE-KD-0403", "Chinese characters in a package/class/method/variable name", "use English identifiers; keep Chinese only in comments and ResManager texts"))
|
|
1092
|
+
package = re.search(r"^\s*package\s+([\w.]+)\s*;", src.code, re.MULTILINE)
|
|
1093
|
+
if not package:
|
|
1094
|
+
issues.append(_issue(src, 0, "java:S1220", "source has no package declaration", "declare the prepared package"))
|
|
1095
|
+
else:
|
|
1096
|
+
if package.group(1).split(".")[0] == "kd":
|
|
1097
|
+
issues.append(_issue(src, package.start(1), "class_fully_qualified_name_conflict_stp",
|
|
1098
|
+
"the kd.* namespace belongs to the standard product; a customization class there can collide with it",
|
|
1099
|
+
"use the developer's ISV/app package prefix prepared for this app", ADVISE))
|
|
1100
|
+
if src.path is not None:
|
|
1101
|
+
parts = tuple(package.group(1).split("."))
|
|
1102
|
+
if src.path.resolve().parent.parts[-len(parts):] != parts:
|
|
1103
|
+
issues.append(_issue(src, package.start(1), "java:S1598", f"package {package.group(1)} does not match the source directory",
|
|
1104
|
+
"move the file or correct the package declaration"))
|
|
1105
|
+
for offset, name, _static, _wild in _imports(src):
|
|
1106
|
+
if name.startswith("kd.") and re.search(r"\.(?:impl|internal)\.", name + "."):
|
|
1107
|
+
issues.append(_issue(src, offset, "CVE-KD-0414-2", f"{name} is not part of the published SDK",
|
|
1108
|
+
"use the public API documented for the target SDK", ADVISE))
|
|
1109
|
+
return issues
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
# --- Security --------------------------------------------------------------------------------
|
|
1113
|
+
|
|
1114
|
+
def _instance_calls(src: _Source, created):
|
|
1115
|
+
"""跟踪同一代码块内的工厂变量,遇到重赋值结束,避免别的实例替它通过检查。"""
|
|
1116
|
+
start = max(src.code.rfind(char, 0, created.start()) for char in ";{}") + 1
|
|
1117
|
+
assigned = re.search(r"(?<![\w$.])([A-Za-z_$][\w$]*)\s*=\s*$", src.code[start:created.start()])
|
|
1118
|
+
if not assigned:
|
|
1119
|
+
return []
|
|
1120
|
+
name = re.escape(assigned.group(1))
|
|
1121
|
+
end = min((close for opening, close in src.pair.items() if src.code[opening] == "{" and opening < created.start() < close), default=len(src.code))
|
|
1122
|
+
after = src.statement_end(created.end()) + 1
|
|
1123
|
+
reassigned = re.search(rf"(?<![\w$.]){name}\s*=(?!=)", src.code[after:end])
|
|
1124
|
+
if reassigned:
|
|
1125
|
+
end = after + reassigned.start()
|
|
1126
|
+
if after >= end:
|
|
1127
|
+
return []
|
|
1128
|
+
return list(re.compile(rf"(?<![\w$.]){name}\s*\.\s*(\w+)\s*\(").finditer(src.code, after, end))
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _xxe_issues(src: _Source) -> list[dict]:
|
|
1132
|
+
issues = []
|
|
1133
|
+
factories = r"\b(DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|TransformerFactory|SchemaFactory)\s*\.\s*newInstance\s*\(|\bnew\s+(SAXReader|SAXBuilder)\s*\("
|
|
1134
|
+
usage = {"newDocumentBuilder", "newSAXParser", "createXMLStreamReader", "createXMLEventReader", "newTransformer", "newTemplates", "newSchema", "read", "build"}
|
|
1135
|
+
for created in re.finditer(factories, src.code):
|
|
1136
|
+
kind = created.group(1) or created.group(2)
|
|
1137
|
+
settings, uncertain = {}, False
|
|
1138
|
+
for call in _instance_calls(src, created):
|
|
1139
|
+
if call.group(1) in usage:
|
|
1140
|
+
break
|
|
1141
|
+
if call.group(1) not in ("setFeature", "setProperty", "setAttribute"):
|
|
1142
|
+
continue
|
|
1143
|
+
arguments = _arguments(src, call.end() - 1)
|
|
1144
|
+
if len(arguments) != 2:
|
|
1145
|
+
continue
|
|
1146
|
+
key = arguments[0].strip('"').rsplit("/", 1)[-1].rsplit(".", 1)[-1]
|
|
1147
|
+
if key not in {"disallow-doctype-decl", "external-general-entities", "external-parameter-entities", "load-external-dtd", "SUPPORT_DTD", "IS_SUPPORTING_EXTERNAL_ENTITIES", "ACCESS_EXTERNAL_DTD", "ACCESS_EXTERNAL_STYLESHEET", "ACCESS_EXTERNAL_SCHEMA"}:
|
|
1148
|
+
continue
|
|
1149
|
+
# 分支中的设置不构成所有路径都安全的证明;运行时值保留为建议项。
|
|
1150
|
+
head = max(src.code.rfind(char, created.end(), call.start()) for char in ";{}") + 1
|
|
1151
|
+
conditional = re.search(r"\b(?:if|else|for|while)\b", src.code[head:call.start()])
|
|
1152
|
+
if conditional or src.brace_depth(call.start()) != src.brace_depth(created.start()) or arguments[1] not in ("true", "false", '""'):
|
|
1153
|
+
uncertain = True
|
|
1154
|
+
settings[key] = None
|
|
1155
|
+
continue
|
|
1156
|
+
settings[key] = arguments[1]
|
|
1157
|
+
if kind == "XMLInputFactory":
|
|
1158
|
+
safe = settings.get("SUPPORT_DTD") == "false" and settings.get("IS_SUPPORTING_EXTERNAL_ENTITIES") == "false"
|
|
1159
|
+
elif kind in ("TransformerFactory", "SchemaFactory"):
|
|
1160
|
+
other = "ACCESS_EXTERNAL_STYLESHEET" if kind == "TransformerFactory" else "ACCESS_EXTERNAL_SCHEMA"
|
|
1161
|
+
safe = settings.get("ACCESS_EXTERNAL_DTD") == '""' and settings.get(other) == '""'
|
|
1162
|
+
else:
|
|
1163
|
+
safe = settings.get("disallow-doctype-decl") == "true" or all(settings.get(key) == "false" for key in (
|
|
1164
|
+
"external-general-entities", "external-parameter-entities", "load-external-dtd"))
|
|
1165
|
+
if not safe:
|
|
1166
|
+
issues.append(_issue(src, created.start(), "java:S2755", "XML parser XXE protection is not verified" if uncertain else "XML parser without XXE protection",
|
|
1167
|
+
"harden this factory before use: DOM/SAX disallow-doctype-decl=true; StAX disable DTD and external entities; transformer/schema deny external DTD and stylesheet/schema access",
|
|
1168
|
+
ADVISE if uncertain else BLOCK))
|
|
1169
|
+
return issues
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
_SECRET_NAME = r"(?:password|passwd|pwd|passphrase)"
|
|
1173
|
+
_TOKEN_NAME = r"(?:secret|token|apikey|api_key|accesskey|access_key|privatekey|private_key|secretkey)"
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _security_issues(src: _Source) -> list[dict]:
|
|
1177
|
+
issues = []
|
|
1178
|
+
# Sonar's credential words are password/passwd/pwd/passphrase; any non-empty literal assigned
|
|
1179
|
+
# to such a name is reported, so a message or field-key constant needs a different name.
|
|
1180
|
+
for match in re.finditer(rf"\b([\w$]*{_SECRET_NAME}[\w$]*)\s*=\s*\"([^\"\n]*)\"", src.lit, re.IGNORECASE):
|
|
1181
|
+
name, value = match.group(1), match.group(2)
|
|
1182
|
+
if not value:
|
|
1183
|
+
issues.append(_issue(src, match.start(), ("FORTIFY:Hardcoded Password", "java:S2068"), f"empty credential assigned to '{name}'",
|
|
1184
|
+
"read credentials from encrypted MC/system parameters at runtime"))
|
|
1185
|
+
continue
|
|
1186
|
+
issues.append(_issue(src, match.start(), ("java:S2068", "FORTIFY:Hardcoded Password"), f"string literal assigned to credential-like name '{name}'",
|
|
1187
|
+
"read real credentials from encrypted MC/system parameters at runtime; if this is a field key or a "
|
|
1188
|
+
f"message, rename the constant without the credential word (e.g. FIELD_{value.upper()} / MSG_...)"
|
|
1189
|
+
if re.fullmatch(r"[a-z][a-z0-9_]*", value) else
|
|
1190
|
+
"read real credentials from encrypted MC/system parameters at runtime; if this is a message, rename "
|
|
1191
|
+
"the constant without the credential word"))
|
|
1192
|
+
for match in re.finditer(rf"\b([\w$]*{_TOKEN_NAME}[\w$]*)\s*=\s*\"([^\"\n]+)\"", src.lit, re.IGNORECASE):
|
|
1193
|
+
issues.append(_issue(src, match.start(), "FORTIFY:Hardcoded Encryption Key", f"'{match.group(1)}' looks like a hard-coded secret",
|
|
1194
|
+
"read secrets from encrypted parameters at runtime; rename the constant if it is not a secret", ADVISE))
|
|
1195
|
+
for offset, text in src.literals:
|
|
1196
|
+
if re.search(r"(?i)\b(?:password|passwd|pwd)\s*=\s*(?![\s'\"?{%:$])[^&\s\"']+", text):
|
|
1197
|
+
issues.append(_issue(src, offset, "java:S2068", "credential embedded in a string", "read credentials from encrypted parameters at runtime"))
|
|
1198
|
+
if re.match(r"(?i)(?:http|ftp|telnet)://(?!localhost|127\.0\.0\.1)", text):
|
|
1199
|
+
issues.append(_issue(src, offset, "java:S5332", "clear-text protocol URL in code", "use https and read external addresses from configuration", ADVISE))
|
|
1200
|
+
if re.fullmatch(r"(?:\d{1,3}\.){3}\d{1,3}(?::\d+)?", text) and not text.startswith(("127.", "0.")):
|
|
1201
|
+
issues.append(_issue(src, offset, "java:S1313", "hard-coded IP address", "read external addresses from configuration", ADVISE))
|
|
1202
|
+
for match in re.finditer(r"\bnew\s+(?:java\.util\.)?Random\s*\(|\bMath\s*\.\s*random\s*\(|\bThreadLocalRandom\s*\.\s*current\s*\(|\bRandomStringUtils\s*\.\s*random", src.code):
|
|
1203
|
+
issues.append(_issue(src, match.start(), ("java:S2245",), "pseudorandom generator", "use java.security.SecureRandom"))
|
|
1204
|
+
for match in re.finditer(r"\bMessageDigest\s*\.\s*getInstance\s*\(\s*\"(MD2|MD4|MD5|SHA|SHA-?1)\"", src.lit, re.IGNORECASE):
|
|
1205
|
+
issues.append(_issue(src, match.start(), "java:S4790", f"weak hash {match.group(1)}", "use SHA-256 or stronger"))
|
|
1206
|
+
for match in re.finditer(r"\b(?:DigestUtils\s*\.\s*(?:md2|md5|sha1?|shaHex|sha1Hex|md5Hex|md2Hex)|Hashing\s*\.\s*(?:md5|sha1))\s*\(", src.code):
|
|
1207
|
+
issues.append(_issue(src, match.start(), "java:S4790", "weak hash", "use SHA-256 or stronger (DigestUtils.sha256Hex)"))
|
|
1208
|
+
for match in re.finditer(r"\bCipher\s*\.\s*getInstance\s*\(\s*\"([^\"]*)\"", src.lit):
|
|
1209
|
+
value = match.group(1).upper()
|
|
1210
|
+
if re.match(r"(?:DES|DESEDE|3DES|TRIPLEDES|RC2|RC4|ARCFOUR|BLOWFISH)\b", value):
|
|
1211
|
+
issues.append(_issue(src, match.start(), "java:S5547", f"weak cipher {match.group(1)}", "use AES/GCM/NoPadding"))
|
|
1212
|
+
elif value.startswith("RSA") and ("OAEP" not in value):
|
|
1213
|
+
issues.append(_issue(src, match.start(), ("FORTIFY:Inadequate RSA Padding", "java:S5542"), f"RSA without OAEP padding ({match.group(1)})", "use RSA/ECB/OAEPWithSHA-256AndMGF1Padding"))
|
|
1214
|
+
elif not value.startswith("RSA") and (value == "AES" or "/ECB" in value or "/CBC/PKCS5PADDING" in value):
|
|
1215
|
+
issues.append(_issue(src, match.start(), "java:S5542", f"insecure cipher mode {match.group(1)}", "use AES/GCM/NoPadding with a random IV"))
|
|
1216
|
+
for match in re.finditer(r"\bSSLContext\s*\.\s*getInstance\s*\(\s*\"([^\"]*)\"", src.lit):
|
|
1217
|
+
if match.group(1) not in ("TLSv1.2", "TLSv1.3", "DTLSv1.2", "DTLSv1.3"):
|
|
1218
|
+
issues.append(_issue(src, match.start(), "java:S4423", f"weak protocol {match.group(1)}", "use TLSv1.2 or TLSv1.3"))
|
|
1219
|
+
for created in re.finditer(r'\b(KeyPairGenerator|KeyGenerator)\s*\.\s*getInstance\s*\(\s*"([^"\n]+)"', src.lit):
|
|
1220
|
+
algorithm = created.group(2).upper()
|
|
1221
|
+
minimum = {"RSA": 2048, "DSA": 2048, "DH": 2048, "DIFFIEHELLMAN": 2048, "EC": 224, "AES": 128}.get(algorithm)
|
|
1222
|
+
for call in _instance_calls(src, created):
|
|
1223
|
+
expected = "initialize" if created.group(1) == "KeyPairGenerator" else "init"
|
|
1224
|
+
arguments = _arguments(src, call.end() - 1)
|
|
1225
|
+
if call.group(1) != expected or not arguments or not re.fullmatch(r"\d+", arguments[0]):
|
|
1226
|
+
continue
|
|
1227
|
+
if minimum is not None and int(arguments[0]) < minimum:
|
|
1228
|
+
issues.append(_issue(src, call.start(), "java:S4426", f"{algorithm} {arguments[0]}-bit key", f"use at least {minimum}-bit {algorithm} keys"))
|
|
1229
|
+
constants = set(re.findall(r"\b([A-Za-z_$][\w$]*)\s*=\s*\"", src.lit))
|
|
1230
|
+
for match in re.finditer(r"\bnew\s+(SecretKeySpec|IvParameterSpec)\s*\(", src.code):
|
|
1231
|
+
arguments = _arguments(src, match.end() - 1)
|
|
1232
|
+
literal_arg = src.lit[match.end():src.pair.get(match.end() - 1, match.end())]
|
|
1233
|
+
first = arguments[0] if arguments else ""
|
|
1234
|
+
root = re.match(r"([A-Za-z_$][\w$]*)", first)
|
|
1235
|
+
if '"' in literal_arg.split(",")[0] or (root and root.group(1) in constants) or re.match(r"new\s+byte\s*\[\s*\]\s*\{", first):
|
|
1236
|
+
code = "FORTIFY:Hardcoded Encryption Key" if match.group(1) == "SecretKeySpec" else "FORTIFY:Insecure Initialization Vector"
|
|
1237
|
+
issues.append(_issue(src, match.start(), code, f"{match.group(1)} built from a constant", "load keys from encrypted configuration; generate a fresh random IV per message with SecureRandom"))
|
|
1238
|
+
issues.extend(_xxe_issues(src))
|
|
1239
|
+
for offset, jump, level in _finally_jumps(src):
|
|
1240
|
+
if jump == "return":
|
|
1241
|
+
issues.append(_issue(src, offset, "FORTIFY:Return Inside Finally", "return inside finally discards exceptions", "move the return out of the finally block", level))
|
|
1242
|
+
log_call = r"\b(?:log|logger|LOG|LOGGER)\s*\.\s*(?:trace|debug|info|warn|error)\s*\(([^;]*)\)\s*;"
|
|
1243
|
+
for match in re.finditer(log_call, src.code):
|
|
1244
|
+
if re.search(rf"(?i)\b\w*(?:{_SECRET_NAME}|{_TOKEN_NAME})\w*\b", match.group(1)):
|
|
1245
|
+
issues.append(_issue(src, match.start(), "FORTIFY:Privacy Violation", "a credential-like value is written to the log", "never log passwords, tokens or keys", ADVISE))
|
|
1246
|
+
return issues
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
# --- Sonar / SpotBugs code rules ---------------------------------------------------------------
|
|
1250
|
+
|
|
1251
|
+
_SIMPLE_RULES = (
|
|
1252
|
+
(r"\bnew\s+(?:java\.lang\.)?(Integer|Long|Short|Byte|Character)\s*\(", ("java:S2129", "DM_NUMBER_CTOR"), "wrapper constructor", "use {0}.valueOf(...)"),
|
|
1253
|
+
(r"\bnew\s+(?:java\.lang\.)?(Double|Float)\s*\(", ("java:S2129", "DM_FP_NUMBER_CTOR"), "wrapper constructor", "use {0}.valueOf(...)"),
|
|
1254
|
+
(r"\bnew\s+(?:java\.lang\.)?(Boolean)\s*\(", ("java:S2129", "DM_BOOLEAN_CTOR"), "Boolean constructor", "use Boolean.valueOf(...) or Boolean.TRUE/FALSE"),
|
|
1255
|
+
(r"\bnew\s+(?:java\.lang\.)?(String)\s*\(\s*\)", ("java:S2129", "DM_STRING_VOID_CTOR"), "new String()", "use \"\""),
|
|
1256
|
+
(r"\bnew\s+(?:java\.math\.)?BigDecimal\s*\(\s*-?\d+[lL]?\s*\)", "java:S2129", "BigDecimal constructed from an integer literal", "use BigDecimal.valueOf(n) or BigDecimal.ZERO/ONE/TEN"),
|
|
1257
|
+
(r"\.\s*equals\s*\(\s*null\s*\)", "EC_NULL_ARG", "equals(null) is always false", "compare with == null"),
|
|
1258
|
+
(r"\.\s*size\s*\(\s*\)\s*(?:==|!=|>|<=)\s*0\b(?!\.)|\.\s*size\s*\(\s*\)\s*(?:>=|<)\s*1\b(?!\.)|\b0\s*(?:==|!=|<|>=)\s*[\w$.()]+\.\s*size\s*\(\s*\)", "java:S1155", "size() compared with 0", "use isEmpty()"),
|
|
1259
|
+
(r"\.\s*to(?:Lower|Upper)Case\s*\(\s*\)\s*\.\s*equals\s*\(", "java:S1157", "case conversion before equals", "use equalsIgnoreCase(...)"),
|
|
1260
|
+
(r"\.\s*to(?:Lower|Upper)Case\s*\(\s*\)", "DM_CONVERT_CASE", "toLowerCase()/toUpperCase() without a Locale", "pass Locale.ROOT (or Locale.ENGLISH)"),
|
|
1261
|
+
(r"\+\s*String\s*\.\s*valueOf\s*\(", "java:S1153", "String.valueOf appended to a String", "append the value directly"),
|
|
1262
|
+
(r"\bnew\s+[\w$.]+\s*(?:<[^;(){}]*>)?\s*\([^;{}]*\)\s*\.\s*getClass\s*\(\s*\)", "java:S2133", "object created only to call getClass", "use X.class"),
|
|
1263
|
+
(r"\.\s*toArray\s*\(\s*new\s+[\w$.]+\s*\[\s*0\s*\]\s*\)", "ITA_INEFFICIENT_TO_ARRAY", "toArray with a zero-length array", "pass an array of the collection size: list.toArray(new X[list.size()])"),
|
|
1264
|
+
(r"\.\s*substring\s*\([^;()]*(?:\([^;()]*\))?[^;()]*\)\s*\.\s*(?:indexOf|lastIndexOf|startsWith)\s*\(", "java:S4635", "substring before indexOf/startsWith", "use indexOf(str, fromIndex) / startsWith(prefix, offset)"),
|
|
1265
|
+
(r"\bthrow\s+new\s+(?:java\.lang\.)?(?:RuntimeException|Exception|Throwable|Error)\s*\(", "java:S112", "generic exception thrown", "throw kd.bos.exception.KDBizException (user-facing) or a dedicated exception type"),
|
|
1266
|
+
(r"(?:==|!=)\s*true\b|(?:==|!=)\s*false\b|\btrue\s*(?:==|!=)|\bfalse\s*(?:==|!=)|\?\s*(?:true|false)\s*:\s*(?:true|false)\b", "java:S1125", "redundant boolean literal", "use the boolean expression directly"),
|
|
1267
|
+
(r"\bnew\s+[\w$.]+\s*(?:<[^;(){}]*>)?\s*\([^;{}]*\)\s*\{\s*\{", "java:S1171", "double-brace initialization", "create the collection, then add the values in ordinary statements"),
|
|
1268
|
+
(r"\bnew\s+(?:Runnable|Callable|Comparator|Supplier|Consumer|BiConsumer|Function|BiFunction|Predicate|BiPredicate|UnaryOperator|BinaryOperator)\s*(?:<[^;{}]*>)?\s*\(\s*\)\s*\{", "java:S1604", "anonymous class for a functional interface", "use a lambda or method reference"),
|
|
1269
|
+
(r"\b(?:Vector|Hashtable|Stack|StringBuffer)\b", "java:S1149", "synchronized legacy collection/StringBuffer", "use ArrayList / HashMap / ArrayDeque / StringBuilder"),
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def _simple_rule_issues(src: _Source) -> list[dict]:
|
|
1274
|
+
issues = []
|
|
1275
|
+
imports_removed = _IMPORT_RE.sub(lambda match: " " * len(match.group(0)), src.code)
|
|
1276
|
+
for pattern, codes, what, fix in _SIMPLE_RULES:
|
|
1277
|
+
for match in re.finditer(pattern, imports_removed):
|
|
1278
|
+
subject = match.group(1) if match.groups() else ""
|
|
1279
|
+
issues.append(_issue(src, match.start(), codes, what, fix.format(subject)))
|
|
1280
|
+
for match in re.finditer(r"\bcatch\s*\(\s*(?:final\s+)?(?:java\.lang\.)?(?:Throwable|Error)\s+(\w+)\s*\)\s*\{", src.code):
|
|
1281
|
+
close = src.pair.get(match.end() - 1)
|
|
1282
|
+
rethrown = close is not None and re.search(rf"\bthrow\s+{match.group(1)}\s*;", src.code[match.end():close])
|
|
1283
|
+
issues.append(_issue(src, match.start(), "java:S1181", "catching Throwable/Error",
|
|
1284
|
+
"catch Exception (or the specific exceptions the code can handle); a transaction rollback belongs in catch (Exception e)",
|
|
1285
|
+
ADVISE if rethrown else BLOCK))
|
|
1286
|
+
for match in re.finditer(r"\bnew\s+(?:java\.lang\.)?String\s*\(\s*\"", src.lit):
|
|
1287
|
+
issues.append(_issue(src, match.start(), ("java:S2129", "DM_STRING_CTOR"), "new String(\"...\")", "use the literal directly"))
|
|
1288
|
+
for match in re.finditer(r'\bnew\s+(?:java\.math\.)?BigInteger\s*\(\s*"(-?\d+)"\s*\)', src.lit):
|
|
1289
|
+
value = int(match.group(1))
|
|
1290
|
+
if -(2 ** 63) <= value < 2 ** 63:
|
|
1291
|
+
issues.append(_issue(src, match.start(), "java:S2129", "BigInteger constructed from a small decimal literal", f"use BigInteger.valueOf({value}L)"))
|
|
1292
|
+
for match in re.finditer(r"\bnew\s+(?:java\.math\.)?BigDecimal\s*\(\s*-?\d+\.\d*(?:[eE][-+]?\d+)?[dDfF]?\s*\)", src.code):
|
|
1293
|
+
issues.append(_issue(src, match.start(), "java:S2111", "BigDecimal(double) is imprecise", "use new BigDecimal(\"1.10\") or BigDecimal.valueOf(d)"))
|
|
1294
|
+
for offset, text in src.literals:
|
|
1295
|
+
close = offset + len(text) + 1
|
|
1296
|
+
if re.search(r"(?:==|!=)\s*$", src.code[max(0, offset - 8):offset]) or re.match(r"\s*(?:==|!=)", src.code[close + 1:close + 9]):
|
|
1297
|
+
issues.append(_issue(src, offset, ("java:S4973", "ES_COMPARING_STRINGS_WITH_EQ"), "string compared with ==/!=", "use \"literal\".equals(value) or Objects.equals"))
|
|
1298
|
+
for match in re.finditer(r"\.\s*replaceAll\s*\(\s*\"((?:\\.|[^\"\\])*)\"", src.lit):
|
|
1299
|
+
if not re.search(r"[.$|()\[\]{}^?*+\\]", match.group(1)):
|
|
1300
|
+
issues.append(_issue(src, match.start(), "java:S5361", "replaceAll with a plain literal", "use replace(...)"))
|
|
1301
|
+
for match in re.finditer(r"\bfor\s*\(\s*(?:final\s+)?[\w$.<>, ?]+\s+(\w+)\s*:\s*([\w$.]+)\s*\.\s*keySet\s*\(\s*\)\s*\)", src.code):
|
|
1302
|
+
body = src.body_after(src.pair.get(src.code.index("(", match.start()), match.end()) + 1)
|
|
1303
|
+
if body and re.search(rf"\b{re.escape(match.group(2))}\s*\.\s*get\s*\(\s*{re.escape(match.group(1))}\s*\)", src.code[body[0]:body[1]]):
|
|
1304
|
+
issues.append(_issue(src, match.start(), ("java:S2864", "WMI_WRONG_MAP_ITERATOR"), "keySet() iteration that also reads the value", "iterate entrySet()"))
|
|
1305
|
+
for match in re.finditer(r"\bcatch\s*\(\s*(?:final\s+)?(?:[\w.]+\s*\|\s*)*(?:java\.lang\.)?InterruptedException\b[^)]*?(\w+)\s*\)\s*\{", src.code):
|
|
1306
|
+
close = src.pair.get(match.end() - 1)
|
|
1307
|
+
block = src.code[match.end():close] if close else ""
|
|
1308
|
+
if not re.search(r"\binterrupt\s*\(\s*\)|\bthrow\b", block):
|
|
1309
|
+
issues.append(_issue(src, match.start(), "java:S2142", "InterruptedException swallowed", "call Thread.currentThread().interrupt() before handling it"))
|
|
1310
|
+
for match in re.finditer(r"\b(?:while\s*\(\s*true\s*\)|for\s*\(\s*;\s*;\s*\))", src.code):
|
|
1311
|
+
body = src.body_after(match.end())
|
|
1312
|
+
if body and not re.search(r"\b(?:break|return|throw)\b|System\s*\.\s*exit", src.code[body[0]:body[1]]):
|
|
1313
|
+
issues.append(_issue(src, match.start(), "java:S2189", "loop without an exit", "add a termination condition"))
|
|
1314
|
+
for match in re.finditer(r"\bif\s*\(\s*(\w+)\s*==\s*null\s*\)\s*\{\s*synchronized\s*\([^)]*\)\s*\{\s*if\s*\(\s*\1\s*==\s*null", src.code):
|
|
1315
|
+
issues.append(_issue(src, match.start(), "java:S2168", "double-checked locking", "initialize eagerly or use a holder class"))
|
|
1316
|
+
locked = [src.body_after(src.pair[match.end() - 1] + 1) for match in re.finditer(r"\bsynchronized\s*\(", src.code)
|
|
1317
|
+
if match.end() - 1 in src.pair]
|
|
1318
|
+
for match in re.finditer(r"\bThread\s*\.\s*sleep\s*\(", src.code):
|
|
1319
|
+
method = src.method_at(match.start())
|
|
1320
|
+
in_block = any(span and span[0] < match.start() < span[1] for span in locked)
|
|
1321
|
+
if (method and method["synchronized"]) or in_block:
|
|
1322
|
+
issues.append(_issue(src, match.start(), "java:S2276", "Thread.sleep while holding a lock", "release the lock first or use wait(timeout)"))
|
|
1323
|
+
for match in re.finditer(r"(?:\{\s*return\s+(true|false)\s*;\s*\}\s*(?:else\s*\{\s*)?|\)\s*return\s+(true|false)\s*;\s*(?:else\s+)?)return\s+(true|false)\s*;", src.code):
|
|
1324
|
+
first = match.group(1) or match.group(2)
|
|
1325
|
+
if first != match.group(3):
|
|
1326
|
+
issues.append(_issue(src, match.start(), "java:S1126", "if/else returning boolean literals", "return the condition directly"))
|
|
1327
|
+
for match in re.finditer(r"(?:^|[;{}])\s*(?:final\s+)?([\w$.]+)(?:\s*<[^;=(){}]*>)?(?:\s*\[\s*\])*\s+([A-Za-z_$][\w$]*)\s*=\s*[^;]+;\s*(?:return|throw)\s+\2\s*;", src.code):
|
|
1328
|
+
if match.group(1) not in _NOT_TYPES:
|
|
1329
|
+
issues.append(_issue(src, match.start(2), "java:S1488", f"'{match.group(2)}' is declared only to be returned", "return the expression directly"))
|
|
1330
|
+
for match in re.finditer(r"\b(?:byte|short|int|long|float|double|char|boolean|[A-Z][\w$]*(?:<[^;=(){}]*>)?)\s+([a-z_$][\w$]*)\s*\[\s*\]\s*[=;,)]", src.code):
|
|
1331
|
+
issues.append(_issue(src, match.start(1), "java:S1197", f"array designator on variable '{match.group(1)}'", "write Type[] name"))
|
|
1332
|
+
for match in re.finditer(r"\b(?:(?:public|protected|private|abstract|static|final|transient|volatile|synchronized|native|strictfp|default)\s+){2,}", src.code):
|
|
1333
|
+
words = match.group(0).split()
|
|
1334
|
+
order = ["public", "protected", "private", "abstract", "static", "final", "transient", "volatile", "synchronized", "native", "strictfp", "default"]
|
|
1335
|
+
ranks = [order.index(word) for word in words]
|
|
1336
|
+
if ranks != sorted(ranks):
|
|
1337
|
+
issues.append(_issue(src, match.start(), "java:S1124", f"modifier order '{' '.join(words)}'", f"write '{' '.join(sorted(words, key=order.index))}'"))
|
|
1338
|
+
return issues
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
def _block_owner(src: _Source, opening: int) -> str:
|
|
1342
|
+
"""The keyword, name or punctuation that a ``{`` belongs to: ``if``, a method name, ``->``, ``=`` ..."""
|
|
1343
|
+
cached = src.owners.get(opening)
|
|
1344
|
+
if cached is not None:
|
|
1345
|
+
return cached
|
|
1346
|
+
code = src.code
|
|
1347
|
+
index = opening - 1
|
|
1348
|
+
while index >= 0 and code[index].isspace():
|
|
1349
|
+
index -= 1
|
|
1350
|
+
if index >= 0 and code[index] == ")" and index in src.opener:
|
|
1351
|
+
index = src.opener[index] - 1
|
|
1352
|
+
while index >= 0 and code[index].isspace():
|
|
1353
|
+
index -= 1
|
|
1354
|
+
owner = _word_ending_at(code, index)
|
|
1355
|
+
elif index >= 1 and code[index - 1:index + 1] == "->":
|
|
1356
|
+
owner = "->"
|
|
1357
|
+
elif index >= 0 and code[index] in "=[],(;{}":
|
|
1358
|
+
owner = code[index]
|
|
1359
|
+
else:
|
|
1360
|
+
owner = _word_ending_at(code, index)
|
|
1361
|
+
src.owners[opening] = owner
|
|
1362
|
+
return owner
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _word_ending_at(code: str, index: int) -> str:
|
|
1366
|
+
end = index + 1
|
|
1367
|
+
while index >= 0 and (code[index].isalnum() or code[index] in "_$"):
|
|
1368
|
+
index -= 1
|
|
1369
|
+
return code[index + 1:end]
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
_CONTROL_OWNERS = {"if", "for", "while", "switch", "catch", "synchronized", "else", "try", "finally", "do"}
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
def _structure_issues(src: _Source) -> list[dict]:
|
|
1376
|
+
issues = []
|
|
1377
|
+
code = src.code
|
|
1378
|
+
for opening, close in src.pair.items():
|
|
1379
|
+
if code[opening] != "{":
|
|
1380
|
+
continue
|
|
1381
|
+
owner = _block_owner(src, opening)
|
|
1382
|
+
if code[opening + 1:close].strip():
|
|
1383
|
+
if owner in (";", "{", "}") and src.method_at(opening):
|
|
1384
|
+
issues.append(_issue(src, opening, "java:S1199", "nested code block", "extract the block into a method or remove the braces"))
|
|
1385
|
+
continue
|
|
1386
|
+
if owner in _CONTROL_OWNERS and not src.raw[opening + 1:close].strip():
|
|
1387
|
+
issues.append(_issue(src, opening, "java:S108", f"empty {owner} block",
|
|
1388
|
+
"handle the case; for a catch, log with LOG.error(message, e) or rethrow — an intentionally empty block needs a comment explaining why"))
|
|
1389
|
+
for method in src.methods:
|
|
1390
|
+
if not src.raw[method["start"]:method["end"]].strip() and not _is_constructor(src, method):
|
|
1391
|
+
issues.append(_issue(src, method["nameOffset"], "java:S1186", f"empty method {method['name']}()",
|
|
1392
|
+
"remove the override if nothing needs to happen, or add a comment explaining why it is intentionally empty"))
|
|
1393
|
+
issues += _switch_issues(src)
|
|
1394
|
+
issues += _collapsible_if_issues(src)
|
|
1395
|
+
for method in src.methods:
|
|
1396
|
+
score = _cognitive_complexity(src, method)
|
|
1397
|
+
if score > 15:
|
|
1398
|
+
issues.append(_issue(src, method["nameOffset"], "java:S3776", f"{method['name']}() has an estimated cognitive complexity of {score} (limit 15)",
|
|
1399
|
+
"extract cohesive steps into private methods and replace nested conditions with early returns", ADVISE))
|
|
1400
|
+
for match in re.finditer(r"\?", code):
|
|
1401
|
+
previous = code[:match.start()].rstrip()[-1:]
|
|
1402
|
+
if previous in ("<", ",", "(") or code[match.start() + 1:match.start() + 2] in (".", ">"):
|
|
1403
|
+
continue
|
|
1404
|
+
statement_start = max(code.rfind(char, 0, match.start()) for char in ";{}") + 1
|
|
1405
|
+
prior = code[statement_start:match.start()]
|
|
1406
|
+
if re.search(r"(?<![<,(])\?(?!\s*[>.,])", prior) and ":" in prior:
|
|
1407
|
+
issues.append(_issue(src, match.start(), "java:S3358", "nested ternary operator", "use if/else or a helper method", ADVISE))
|
|
1408
|
+
for match in re.finditer(r"\b(?:if|while)\s*\(", code):
|
|
1409
|
+
close = src.pair.get(match.end() - 1)
|
|
1410
|
+
condition = code[match.end():close] if close else ""
|
|
1411
|
+
if re.search(r"(?<![&|])[&|](?![&|=])", condition) and re.search(r"==|!=|<|>|!\w|\bequals\b|\bisEmpty\b|\btrue\b|\bfalse\b", condition):
|
|
1412
|
+
issues.append(_issue(src, match.start(), "java:S2178", "non-short-circuit & or | in a condition", "use && / ||", ADVISE))
|
|
1413
|
+
for match in re.finditer(r"\bcatch\s*\([^)]*?(\w+)\s*\)\s*\{", code):
|
|
1414
|
+
close = src.pair.get(match.end() - 1)
|
|
1415
|
+
block = code[match.end():close] if close else ""
|
|
1416
|
+
variable = re.escape(match.group(1))
|
|
1417
|
+
if (re.search(rf"\.\s*(?:error|warn|info)\s*\([^;]*\b{variable}\b", block)
|
|
1418
|
+
and re.search(rf"\bthrow\s+(?:{variable}\b|new\s+\w+\s*\([^;]*\b{variable}\b)", block)):
|
|
1419
|
+
issues.append(_issue(src, match.start(), "java:S2139", "exception both logged and rethrown",
|
|
1420
|
+
"either log and handle it, or rethrow it with the cause — not both", ADVISE))
|
|
1421
|
+
return issues
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _is_constructor(src: _Source, method: dict) -> bool:
|
|
1425
|
+
"""A declaration with no return type left once annotations, modifiers and type parameters go."""
|
|
1426
|
+
rest = re.sub(r"@[\w.$]+(?:\s*\([^()]*\))?|<[^<>]*>|\b(?:public|protected|private|static|final|synchronized|abstract|native|strictfp)\b",
|
|
1427
|
+
" ", method["prefix"])
|
|
1428
|
+
return not rest.strip()
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def _parameter_names(params: str) -> list[str]:
|
|
1432
|
+
names = []
|
|
1433
|
+
for part in _split_params(params):
|
|
1434
|
+
name = re.search(r"([A-Za-z_$][\w$]*)\s*(?:\[\s*\])*$", part)
|
|
1435
|
+
if name:
|
|
1436
|
+
names.append(name.group(1))
|
|
1437
|
+
return names
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _switch_issues(src: _Source) -> list[dict]:
|
|
1441
|
+
issues = []
|
|
1442
|
+
code = src.code
|
|
1443
|
+
for match in re.finditer(r"\bswitch\s*\(", code):
|
|
1444
|
+
close = src.pair.get(match.end() - 1)
|
|
1445
|
+
body = src.body_after(close + 1) if close is not None else None
|
|
1446
|
+
if not body or code[body[0]] != "{":
|
|
1447
|
+
continue
|
|
1448
|
+
before = code[:match.start()].rstrip()[-2:]
|
|
1449
|
+
expression = before.endswith(("=", "(", ",", "->")) or re.search(r"\b(?:return|yield)\s*$", code[:match.start()])
|
|
1450
|
+
labels = []
|
|
1451
|
+
index = body[0] + 1
|
|
1452
|
+
while index < body[1]:
|
|
1453
|
+
char = code[index]
|
|
1454
|
+
if char in "({[" and index in src.pair:
|
|
1455
|
+
index = src.pair[index] + 1
|
|
1456
|
+
continue
|
|
1457
|
+
label = re.match(r"\b(?:case\b[^;{}]*?|default\s*)(:|->)", code[index:body[1]])
|
|
1458
|
+
if label and (index == 0 or not re.match(r"[\w$]", code[index - 1])):
|
|
1459
|
+
labels.append((index, index + label.end(), label.group(1)))
|
|
1460
|
+
index += label.end()
|
|
1461
|
+
continue
|
|
1462
|
+
index += 1
|
|
1463
|
+
if not expression and not any(re.match(r"default\b", code[start:end]) for start, end, _arrow in labels):
|
|
1464
|
+
issues.append(_issue(src, match.start(), "java:S131", "switch without a default clause", "add a default branch (handle or reject the unexpected value)"))
|
|
1465
|
+
if any(arrow == "->" for _start, _end, arrow in labels):
|
|
1466
|
+
continue
|
|
1467
|
+
for position, (_start, end, _arrow) in enumerate(labels[:-1]):
|
|
1468
|
+
segment = code[end:labels[position + 1][0]].strip()
|
|
1469
|
+
if not segment:
|
|
1470
|
+
continue
|
|
1471
|
+
last = _last_statement(segment)
|
|
1472
|
+
if not re.match(r"(?:break|return|throw|continue|yield)\b", last):
|
|
1473
|
+
level = ADVISE if segment.endswith("}") else BLOCK
|
|
1474
|
+
issues.append(_issue(src, labels[position + 1][0], "java:S128", "switch case falls through to the next label",
|
|
1475
|
+
"end each case with break/return/throw", level))
|
|
1476
|
+
return issues
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def _last_statement(segment: str) -> str:
|
|
1480
|
+
text = segment.strip()
|
|
1481
|
+
while text.startswith("{") and text.endswith("}"):
|
|
1482
|
+
text = text[1:-1].strip()
|
|
1483
|
+
if text.endswith("}"):
|
|
1484
|
+
return text
|
|
1485
|
+
statements = [part.strip() for part in re.split(r"[;{}]", text[:-1] if text.endswith(";") else text)]
|
|
1486
|
+
return statements[-1] if statements else ""
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
def _collapsible_if_issues(src: _Source) -> list[dict]:
|
|
1490
|
+
issues = []
|
|
1491
|
+
code = src.code
|
|
1492
|
+
for match in re.finditer(r"\bif\s*\(", code):
|
|
1493
|
+
if re.search(r"\belse\s*$", code[:match.start()]):
|
|
1494
|
+
continue
|
|
1495
|
+
close = src.pair.get(match.end() - 1)
|
|
1496
|
+
body = src.body_after(close + 1) if close is not None else None
|
|
1497
|
+
if not body or code[body[0]] != "{" or re.match(r"\s*else\b", code[body[1] + 1:]):
|
|
1498
|
+
continue
|
|
1499
|
+
inner = code[body[0] + 1:body[1]]
|
|
1500
|
+
inner_match = re.match(r"\s*if\s*\(", inner)
|
|
1501
|
+
if not inner_match:
|
|
1502
|
+
continue
|
|
1503
|
+
inner_open = body[0] + 1 + inner_match.end() - 1
|
|
1504
|
+
inner_close = src.pair.get(inner_open)
|
|
1505
|
+
inner_body = src.body_after(inner_close + 1) if inner_close is not None else None
|
|
1506
|
+
if not inner_body:
|
|
1507
|
+
continue
|
|
1508
|
+
rest = code[inner_body[1] + 1:body[1]]
|
|
1509
|
+
if not rest.strip() or (code[inner_body[0]] != "{" and not code[inner_body[1] + 1:body[1]].strip()):
|
|
1510
|
+
if not re.match(r"\s*else\b", code[inner_body[1] + 1:]):
|
|
1511
|
+
issues.append(_issue(src, match.start(), "java:S1066", "nested if can be merged with its parent", "combine the conditions with &&"))
|
|
1512
|
+
return issues
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _cognitive_complexity(src: _Source, method: dict) -> int:
|
|
1516
|
+
code = src.code
|
|
1517
|
+
start, end = method["start"], method["end"]
|
|
1518
|
+
nesting_blocks = [(opening, close) for opening, close in src.pair.items()
|
|
1519
|
+
if start <= opening < end and code[opening] == "{"
|
|
1520
|
+
and _block_owner(src, opening) in {"if", "else", "for", "while", "do", "switch", "catch", "->"}]
|
|
1521
|
+
|
|
1522
|
+
def nesting(offset: int) -> int:
|
|
1523
|
+
return sum(1 for opening, close in nesting_blocks if opening < offset < close)
|
|
1524
|
+
|
|
1525
|
+
score = 0
|
|
1526
|
+
body = code[start:end]
|
|
1527
|
+
for match in re.finditer(r"\belse\s+if\b|\belse\b|\bif\b|\bfor\b|\bwhile\b|\bdo\b|\bswitch\b|\bcatch\b|\b(?:break|continue)\s+[A-Za-z_$]", body):
|
|
1528
|
+
offset = start + match.start()
|
|
1529
|
+
token = match.group(0).split()[0]
|
|
1530
|
+
if token == "else":
|
|
1531
|
+
score += 1
|
|
1532
|
+
continue
|
|
1533
|
+
if token in ("break", "continue"):
|
|
1534
|
+
score += 1
|
|
1535
|
+
continue
|
|
1536
|
+
if token == "while" and re.search(r"\}\s*$", code[:offset]):
|
|
1537
|
+
opening = src.opener.get(code[:offset].rstrip().__len__() - 1)
|
|
1538
|
+
if opening is not None and _block_owner(src, opening) == "do":
|
|
1539
|
+
continue
|
|
1540
|
+
score += 1 + nesting(offset)
|
|
1541
|
+
for match in re.finditer(r"\?", body):
|
|
1542
|
+
offset = start + match.start()
|
|
1543
|
+
previous = code[:offset].rstrip()[-1:]
|
|
1544
|
+
if previous in ("<", ",", "(") or code[offset + 1:offset + 2] in (".", ">"):
|
|
1545
|
+
continue
|
|
1546
|
+
score += 1 + nesting(offset)
|
|
1547
|
+
previous_operator, previous_end = None, -1
|
|
1548
|
+
for match in re.finditer(r"&&|\|\||[;{}()]", body):
|
|
1549
|
+
token = match.group(0)
|
|
1550
|
+
if token in ("&&", "||"):
|
|
1551
|
+
if token != previous_operator:
|
|
1552
|
+
score += 1
|
|
1553
|
+
previous_operator = token
|
|
1554
|
+
continue
|
|
1555
|
+
if token in ";{}":
|
|
1556
|
+
previous_operator = None
|
|
1557
|
+
return score
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
# --- Declarations: naming, unused members, statics, utility classes --------------------------
|
|
1561
|
+
|
|
1562
|
+
_FIELD_RE = re.compile(
|
|
1563
|
+
r"(?:^|(?<=[;{}]))\s*(?:@[\w.$]+(?:\s*\([^()]*\))?\s*)*"
|
|
1564
|
+
r"(?P<mods>(?:(?:public|protected|private|static|final|transient|volatile)\s+)*)"
|
|
1565
|
+
r"(?P<type>[\w$.]+(?:\s*<[^;=(){}]*>)?(?:\s*\[\s*\])*)\s+(?P<name>[A-Za-z_$][\w$]*)\s*(?P<end>[=;,])"
|
|
1566
|
+
)
|
|
1567
|
+
_CONSTANT_TYPES = {"byte", "short", "int", "long", "float", "double", "boolean", "char", "String",
|
|
1568
|
+
"Byte", "Short", "Integer", "Long", "Float", "Double", "Boolean", "Character"}
|
|
1569
|
+
_NOT_TYPES = {"return", "throw", "new", "else", "case", "yield", "assert", "package", "import", "goto", "break", "continue"}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def _class_depth(src: _Source, offset: int) -> int:
|
|
1573
|
+
return src.brace_depth(offset)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
def _fields(src: _Source) -> list[dict]:
|
|
1577
|
+
fields = []
|
|
1578
|
+
for match in _FIELD_RE.finditer(src.code):
|
|
1579
|
+
if match.group("type") in _NOT_TYPES or _class_depth(src, match.start("type")) != 1:
|
|
1580
|
+
continue
|
|
1581
|
+
mods = match.group("mods").split()
|
|
1582
|
+
fields.append({"name": match.group("name"), "type": re.sub(r"\s+", "", match.group("type")), "mods": set(mods),
|
|
1583
|
+
"offset": match.start("name"), "end": match.group("end")})
|
|
1584
|
+
return fields
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def _declaration_issues(src: _Source) -> list[dict]:
|
|
1588
|
+
issues = []
|
|
1589
|
+
code = src.code
|
|
1590
|
+
fields = _fields(src)
|
|
1591
|
+
imports_removed = _IMPORT_RE.sub(lambda match: " " * len(match.group(0)), code)
|
|
1592
|
+
for field in fields:
|
|
1593
|
+
name, mods = field["name"], field["mods"]
|
|
1594
|
+
if {"static", "final"} <= mods:
|
|
1595
|
+
if field["type"] in _CONSTANT_TYPES and name != "serialVersionUID" and not re.fullmatch(r"[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*", name):
|
|
1596
|
+
issues.append(_issue(src, field["offset"], "java:S115", f"constant '{name}' is not UPPER_SNAKE_CASE", "rename it, e.g. " + re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", name).upper()))
|
|
1597
|
+
elif not re.fullmatch(r"[a-z][a-zA-Z0-9]*", name):
|
|
1598
|
+
issues.append(_issue(src, field["offset"], "java:S116", f"field '{name}' is not lowerCamelCase", "rename it in lowerCamelCase"))
|
|
1599
|
+
if "private" in mods and name != "serialVersionUID" and len(re.findall(rf"(?<![\w$]){re.escape(name)}(?![\w$])", imports_removed)) == 1:
|
|
1600
|
+
issues.append(_issue(src, field["offset"], ("java:S1068", "UUF_UNUSED_FIELD"), f"private field '{name}' is never used", "remove it"))
|
|
1601
|
+
if "static" in mods and "final" not in mods:
|
|
1602
|
+
if "public" in mods:
|
|
1603
|
+
issues.append(_issue(src, field["offset"], ("java:S1444", "java:S2386"), f"public static field '{name}' is mutable", "make it private static final, or an instance field"))
|
|
1604
|
+
for method in src.methods:
|
|
1605
|
+
if method["static"]:
|
|
1606
|
+
continue
|
|
1607
|
+
body = code[method["start"]:method["end"]]
|
|
1608
|
+
write = re.search(rf"(?<![\w$.])(?:this\.)?{re.escape(name)}\s*(?:[+\-*/%&|^]?=(?!=)|\+\+|--)|(?:\+\+|--){re.escape(name)}\b", body)
|
|
1609
|
+
if write:
|
|
1610
|
+
issues.append(_issue(src, method["start"] + write.start(), "java:S2696",
|
|
1611
|
+
f"instance method {method['name']}() writes static field '{name}'; the plugin instance is shared across users",
|
|
1612
|
+
"keep the state in a local variable or getPageCache()"))
|
|
1613
|
+
break
|
|
1614
|
+
for match in re.finditer(r"\b(?:class|interface|enum|record)\s+([A-Za-z_$][\w$]*)(?![\w$.])", code):
|
|
1615
|
+
if not re.fullmatch(r"[A-Z][a-zA-Z0-9]*", match.group(1)):
|
|
1616
|
+
issues.append(_issue(src, match.start(1), "java:S101", f"class '{match.group(1)}' is not UpperCamelCase", "rename the class (and the file)"))
|
|
1617
|
+
methods_by_lower: dict[str, set[str]] = {}
|
|
1618
|
+
for method in src.methods:
|
|
1619
|
+
constructor = _is_constructor(src, method)
|
|
1620
|
+
if not constructor and not re.fullmatch(r"[a-z][a-zA-Z0-9]*", method["name"]):
|
|
1621
|
+
issues.append(_issue(src, method["nameOffset"], "java:S100", f"method '{method['name']}' is not lowerCamelCase", "rename it in lowerCamelCase"))
|
|
1622
|
+
if not constructor:
|
|
1623
|
+
methods_by_lower.setdefault(method["name"].lower(), set()).add(method["name"])
|
|
1624
|
+
for name in _parameter_names(method["params"]):
|
|
1625
|
+
if not re.fullmatch(r"[a-z][a-zA-Z0-9]*", name):
|
|
1626
|
+
issues.append(_issue(src, method["nameOffset"], "java:S117", f"parameter '{name}' is not lowerCamelCase", "rename it in lowerCamelCase"))
|
|
1627
|
+
body = code[method["start"]:method["end"]]
|
|
1628
|
+
resources = [(method["start"] + match.end() - 1, src.pair.get(method["start"] + match.end() - 1, -1))
|
|
1629
|
+
for match in re.finditer(r"\btry\s*\(", body)]
|
|
1630
|
+
for local in re.finditer(r"(?:^|(?<=[;{}(]))\s*(?:final\s+)?([A-Za-z_$][\w$.]*(?:\s*<[^;=(){}]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_$][\w$]*)\s*(=(?!=)|;)", body):
|
|
1631
|
+
if local.group(1) in _NOT_TYPES:
|
|
1632
|
+
continue
|
|
1633
|
+
name = local.group(2)
|
|
1634
|
+
offset = method["start"] + local.start(2)
|
|
1635
|
+
if not re.fullmatch(r"[a-z][a-zA-Z0-9]*", name):
|
|
1636
|
+
issues.append(_issue(src, offset, "java:S117", f"local variable '{name}' is not lowerCamelCase", "rename it in lowerCamelCase"))
|
|
1637
|
+
if (len(re.findall(rf"(?<![\w$]){re.escape(name)}(?![\w$])", body)) == 1
|
|
1638
|
+
and not any(opening < offset < close for opening, close in resources)):
|
|
1639
|
+
issues.append(_issue(src, offset, "java:S1481", f"local variable '{name}' is never used", "remove it"))
|
|
1640
|
+
if (method["private"] or method["static"]) and not method["override"] and not constructor and method["name"] != "main":
|
|
1641
|
+
for name in _parameter_names(method["params"]):
|
|
1642
|
+
if not re.search(rf"(?<![\w$]){re.escape(name)}(?![\w$])", body):
|
|
1643
|
+
issues.append(_issue(src, method["nameOffset"], "java:S1172", f"parameter '{name}' of {method['name']}() is never used", "remove the parameter"))
|
|
1644
|
+
if method["private"] and not constructor and method["name"] not in ("readObject", "writeObject", "readResolve", "writeReplace", "readObjectNoData"):
|
|
1645
|
+
uses = len(re.findall(rf"(?<![\w$]){re.escape(method['name'])}\s*\(|::\s*{re.escape(method['name'])}\b", imports_removed))
|
|
1646
|
+
declarations = sum(1 for item in src.methods if item["name"] == method["name"])
|
|
1647
|
+
if uses <= declarations:
|
|
1648
|
+
issues.append(_issue(src, method["nameOffset"], "java:S1144", f"private method {method['name']}() is never called", "remove it"))
|
|
1649
|
+
returns = re.sub(r"@[\w.]+(?:\s*\([^)]*\))?|\b(?:public|protected|private|static|final|synchronized|abstract|native|strictfp|default)\b", "", method["prefix"]).strip()
|
|
1650
|
+
if not method["override"] and re.search(r"<\s*\?|\?\s*(?:extends|super)\b", returns):
|
|
1651
|
+
issues.append(_issue(src, method["nameOffset"], "java:S1452", f"{method['name']}() returns a wildcard type", "return a concrete generic type"))
|
|
1652
|
+
if re.search(r"\b(?:List|Map|Set|Collection|ArrayList|HashMap|HashSet|LinkedList|LinkedHashMap)\b|\[\s*\]\s*$", returns):
|
|
1653
|
+
for match in re.finditer(r"\breturn\s+null\s*;", body):
|
|
1654
|
+
offset = method["start"] + match.start()
|
|
1655
|
+
if _nested_function(src, offset, method["start"]):
|
|
1656
|
+
continue
|
|
1657
|
+
issues.append(_issue(src, offset, "java:S1168", f"{method['name']}() returns null for a collection/array", "return an empty collection or array"))
|
|
1658
|
+
break
|
|
1659
|
+
field_names = {field["name"] for field in fields}
|
|
1660
|
+
lowered: dict[str, set[str]] = {}
|
|
1661
|
+
for name in field_names:
|
|
1662
|
+
lowered.setdefault(name.lower(), set()).add(name)
|
|
1663
|
+
for key, names in methods_by_lower.items():
|
|
1664
|
+
lowered.setdefault(key, set()).update(names)
|
|
1665
|
+
for key, names in lowered.items():
|
|
1666
|
+
clash = len(names) > 1 or (names & field_names and key in methods_by_lower and methods_by_lower[key] & field_names)
|
|
1667
|
+
if clash:
|
|
1668
|
+
field = next((item for item in fields if item["name"].lower() == key), None)
|
|
1669
|
+
offset = field["offset"] if field else next(item["nameOffset"] for item in src.methods if item["name"].lower() == key)
|
|
1670
|
+
issues.append(_issue(src, offset, "java:S1845", f"members {sorted(names)} differ only by capitalization or reuse a field name", "rename one of them"))
|
|
1671
|
+
declares_equals = re.search(r"\bboolean\s+equals\s*\(\s*(?:final\s+)?Object\b", code)
|
|
1672
|
+
declares_hash = re.search(r"\bint\s+hashCode\s*\(\s*\)", code)
|
|
1673
|
+
if bool(declares_equals) != bool(declares_hash):
|
|
1674
|
+
issues.append(_issue(src, (declares_equals or declares_hash).start(), "java:S1206", "equals and hashCode are not overridden together", "override both"))
|
|
1675
|
+
top = re.search(r"\bclass\s+([A-Za-z_$][\w$]*)([^{]*)\{", code)
|
|
1676
|
+
if top and not re.search(r"\b(?:extends|implements)\b", top.group(2)) and not re.search(r"\babstract\s+class\b", code[:top.end()]):
|
|
1677
|
+
outer = [method for method in src.methods if not _is_constructor(src, method)]
|
|
1678
|
+
has_constructor = any(_is_constructor(src, method) for method in src.methods)
|
|
1679
|
+
instance_fields = [field for field in fields if "static" not in field["mods"]]
|
|
1680
|
+
if outer and all(method["static"] for method in outer) and not has_constructor and not instance_fields:
|
|
1681
|
+
issues.append(_issue(src, top.start(), "java:S1118", f"utility class {top.group(1)} has an implicit public constructor",
|
|
1682
|
+
f"add 'private {top.group(1)}() {{ }}' with a comment, e.g. // static helpers only"))
|
|
1683
|
+
for offset, name, static, wild in _imports(src):
|
|
1684
|
+
if wild:
|
|
1685
|
+
continue
|
|
1686
|
+
simple = name.rsplit(".", 1)[-1]
|
|
1687
|
+
package = name.rsplit(".", 1)[0]
|
|
1688
|
+
own_package = re.search(r"^\s*package\s+([\w.]+)\s*;", code, re.MULTILINE)
|
|
1689
|
+
redundant = (not static and (package == "java.lang" or (own_package and package == own_package.group(1))))
|
|
1690
|
+
used = re.search(rf"(?<![\w$]){re.escape(simple)}(?![\w$])", imports_removed) or re.search(
|
|
1691
|
+
rf"(?:@link|@see|@throws|@exception)\s+[\w.#]*\b{re.escape(simple)}\b", src.raw)
|
|
1692
|
+
if redundant or not used:
|
|
1693
|
+
issues.append(_issue(src, offset, "java:S1128", f"unnecessary import {name}", "remove the import"))
|
|
1694
|
+
imported = [name for _offset, name, _static, wild in _imports(src) if not wild]
|
|
1695
|
+
for name in {item for item in imported if imported.count(item) > 1}:
|
|
1696
|
+
offset = [item for item in _imports(src) if item[1] == name][1][0]
|
|
1697
|
+
issues.append(_issue(src, offset, "java:S1128", f"duplicate import {name}", "remove the duplicate"))
|
|
1698
|
+
return issues
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
# --- Plugin lifecycle: propertyChanged / updateView ------------------------------------------
|
|
1702
|
+
|
|
1703
|
+
def _lifecycle_issues(src: _Source) -> list[dict]:
|
|
1704
|
+
"""updateView() in propertyChanged() must sit behind a check of which property changed.
|
|
1705
|
+
|
|
1706
|
+
Guarded means inside an if/else/switch block, after an unbraced ``if (...)``, or after an early
|
|
1707
|
+
``return`` that filters the property first.
|
|
1708
|
+
"""
|
|
1709
|
+
issues = []
|
|
1710
|
+
for method in src.methods:
|
|
1711
|
+
if method["name"] != "propertyChanged":
|
|
1712
|
+
continue
|
|
1713
|
+
for match in re.finditer(r"\bupdateView\s*\(", src.code[method["start"]:method["end"]]):
|
|
1714
|
+
offset = method["start"] + match.start()
|
|
1715
|
+
guarded = any(_block_owner(src, opening) in ("if", "switch", "else") and opening < offset < close
|
|
1716
|
+
for opening, close in src.pair.items() if method["start"] <= opening < method["end"] and src.code[opening] == "{")
|
|
1717
|
+
statement = src.code[max(src.code.rfind(char, method["start"], offset) for char in ";{}") + 1:offset]
|
|
1718
|
+
unbraced = re.match(r"\s*(?:if\s*\(|else\b)", statement)
|
|
1719
|
+
early_return = re.search(r"\breturn\b", src.code[method["start"]:offset])
|
|
1720
|
+
if not guarded and not unbraced and not early_return:
|
|
1721
|
+
issues.append(_issue(src, offset, "CVE-KD-2026-0618", "propertyChanged() calls updateView() unconditionally",
|
|
1722
|
+
"guard it on e.getProperty().getName() and refresh only the changed control: getView().updateView(\"fieldKey\")"))
|
|
1723
|
+
return issues
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
# --- Comments ---------------------------------------------------------------------------------
|
|
1727
|
+
|
|
1728
|
+
_CODE_COMMENT_RE = re.compile(
|
|
1729
|
+
r"^(?:(?:[\w$.<>\[\]]+\s+)?[\w$.\[\]()<>]+\s*(?:[+\-*/]?=)\s*[^=].*;|[\w$.]+\s*\(.*\)\s*;|(?:if|for|while|switch)\s*\(.*\)\s*\{?|return\b.*;|\}\s*(?:else\b.*)?|import\s+[\w.]+;)\s*$"
|
|
1730
|
+
)
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
def _comment_issues(src: _Source) -> list[dict]:
|
|
1734
|
+
issues = []
|
|
1735
|
+
lines = src.raw.split("\n")
|
|
1736
|
+
lit_lines = src.lit.split("\n")
|
|
1737
|
+
offset = 0
|
|
1738
|
+
in_javadoc = False
|
|
1739
|
+
for raw_line, lit_line in zip(lines, lit_lines):
|
|
1740
|
+
# `lit` blanks comments only, so the characters it blanked are the comment on this line.
|
|
1741
|
+
comment = "".join(char for char, kept in zip(raw_line, lit_line) if kept == " ")
|
|
1742
|
+
if "/**" in comment:
|
|
1743
|
+
in_javadoc = True
|
|
1744
|
+
text = re.sub(r"^\s*(?://+|/\*+|\*+)\s*", "", comment.strip()).rstrip("*/ ").strip()
|
|
1745
|
+
if not in_javadoc and text and _CODE_COMMENT_RE.match(text):
|
|
1746
|
+
issues.append(_issue(src, offset, "java:S125", "commented-out code", "delete the commented code; version control keeps history", ADVISE))
|
|
1747
|
+
if "*/" in comment:
|
|
1748
|
+
in_javadoc = False
|
|
1749
|
+
offset += len(raw_line) + 1
|
|
1750
|
+
return issues
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
# --- Duplicated literals ----------------------------------------------------------------------
|
|
1754
|
+
|
|
1755
|
+
def _duplicate_literal_issues(src: _Source) -> list[dict]:
|
|
1756
|
+
occurrences: dict[str, list[int]] = {}
|
|
1757
|
+
for offset, text in src.literals:
|
|
1758
|
+
if len(text) < 5:
|
|
1759
|
+
continue
|
|
1760
|
+
line_start = src.lit.rfind("\n", 0, offset) + 1
|
|
1761
|
+
if re.search(r"@[\w.]+\s*\([^)]*$", src.lit[line_start:offset]):
|
|
1762
|
+
continue
|
|
1763
|
+
occurrences.setdefault(text, []).append(offset)
|
|
1764
|
+
issues = []
|
|
1765
|
+
for text, offsets in occurrences.items():
|
|
1766
|
+
if len(offsets) >= 3:
|
|
1767
|
+
lines = sorted({src.line(offset) for offset in offsets})
|
|
1768
|
+
issues.append(_issue(src, offsets[0], "java:S1192", f"literal \"{text}\" appears {len(offsets)} times (lines {lines})",
|
|
1769
|
+
"declare it once as a private static final String constant (e.g. for a field key) and use the constant"))
|
|
1770
|
+
return issues
|
|
1771
|
+
|
|
1772
|
+
|
|
1773
|
+
_CHECKS = (
|
|
1774
|
+
_forbidden_type_issues, _forbidden_call_issues, _static_collection_issues, _loop_issues,
|
|
1775
|
+
_data_access_issues, _suppression_and_naming_issues, _security_issues, _simple_rule_issues,
|
|
1776
|
+
_structure_issues, _declaration_issues, _lifecycle_issues, _comment_issues, _duplicate_literal_issues,
|
|
1777
|
+
_catalogue_issues, _additional_security_issues, _additional_code_issues,
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def scan_source(source_text: str, path: Path | None = None) -> list[dict]:
|
|
1782
|
+
"""Every DCS issue in one Java source, ordered by line; ``level`` tells block from advise."""
|
|
1783
|
+
src = _Source(source_text, path)
|
|
1784
|
+
issues = [issue for check in _CHECKS for issue in check(src)]
|
|
1785
|
+
unique = {(issue["line"], tuple(issue["ruleCodes"]), issue["message"]): issue for issue in issues}
|
|
1786
|
+
return sorted(unique.values(), key=lambda issue: (issue["line"], issue["ruleCodes"][0]))
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def dcs_rule_issues(source_text: str, path: Path | None = None) -> list[dict]:
|
|
1790
|
+
"""Blocking DCS findings, in the shape of the other compile_plugin_source checks."""
|
|
1791
|
+
return [issue for issue in scan_source(source_text, path) if issue["level"] == BLOCK]
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def dcs_rule_warnings(source_text: str, path: Path | None = None) -> list[dict]:
|
|
1795
|
+
"""Advisory DCS findings for ``sourceWarnings``: not provable from source, still to be fixed."""
|
|
1796
|
+
return [{**issue, "severity": "warning", "errorCode": "PLUGIN_SOURCE_DCS_RULE_ADVISORY"}
|
|
1797
|
+
for issue in scan_source(source_text, path) if issue["level"] == ADVISE]
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
# --- SQL scripts and packaged files (module scan) --------------------------------------------
|
|
1801
|
+
|
|
1802
|
+
_SQL_RULES = (
|
|
1803
|
+
(r"\bcreate\s+(?:or\s+replace\s+)?view\b", "CVE-KD-SQL-0001"),
|
|
1804
|
+
(r"\bcreate\s+(?:or\s+replace\s+)?procedure\b", "CVE-KD-SQL-0002"),
|
|
1805
|
+
(r"\bcreate\s+(?:or\s+replace\s+)?function\b", "CVE-KD-SQL-0003"),
|
|
1806
|
+
(r"\bcreate\s+(?:or\s+replace\s+)?trigger\b", "CVE-KD-SQL-0004"),
|
|
1807
|
+
(r"\binsert\s+into\b", "CVE-KD-SQL-0005"),
|
|
1808
|
+
(r"\bupdate\s+[\w.]+\s+set\b", "CVE-KD-SQL-0006"),
|
|
1809
|
+
(r"\bdelete\s+(?:from\s+)?[\w.]+\b", "CVE-KD-SQL-0007"),
|
|
1810
|
+
(r"\balter\s+table\s+[\w.]+\s+drop\b", "CVE-KD-SQL-0008"),
|
|
1811
|
+
(r"\bdrop\s+table\b", "CVE-KD-SQL-0009"),
|
|
1812
|
+
(r"\brename\s+(?:table|column)\b|\balter\s+table\s+[\w.]+\s+rename\b|\b(?:sp_rename|p_modifyobjectname)\b", "CVE-KD-SQL-0010"),
|
|
1813
|
+
)
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
_SQL_TOKEN_RE = re.compile(r"'(?:''|[^'])*'|\"(?:\"\"|[^\"])*\"|`(?:``|[^`])*`|\[(?:\]\]|[^\]])*\]|--[^\r\n]*|/\*[\s\S]*?\*/|(?P<tag>\$(?:[A-Za-z_]\w*)?\$)[\s\S]*?(?P=tag)")
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def _sql_code(text: str) -> tuple[str, dict[int, str]]:
|
|
1820
|
+
"""按 SQL 词法顺序跳过字符串和注释;引号标识符保留位置但不能触发关键字规则。"""
|
|
1821
|
+
identifiers = {}
|
|
1822
|
+
|
|
1823
|
+
def blank(match):
|
|
1824
|
+
token = match.group(0)
|
|
1825
|
+
if token[0] in '\"`[':
|
|
1826
|
+
closing = "]" if token[0] == "[" else token[0]
|
|
1827
|
+
identifiers[match.start()] = token[1:-1].replace(closing * 2, closing)
|
|
1828
|
+
return re.sub(r"[^\r\n]", "x", token)
|
|
1829
|
+
return re.sub(r"[^\r\n]", " ", token)
|
|
1830
|
+
|
|
1831
|
+
return _SQL_TOKEN_RE.sub(blank, text), identifiers
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
def scan_sql(data: bytes) -> list[dict]:
|
|
1835
|
+
try:
|
|
1836
|
+
text = data.decode("utf-8-sig")
|
|
1837
|
+
except UnicodeDecodeError:
|
|
1838
|
+
return [{"line": 1, "ruleCodes": ["CVE-KD-SQL-0015"], "dcsRisk": RULES["CVE-KD-SQL-0015"][0], "dcsRule": RULES["CVE-KD-SQL-0015"][1],
|
|
1839
|
+
"level": BLOCK, "message": "SQL script is not UTF-8", "fix": "save the script as UTF-8"}]
|
|
1840
|
+
code, identifiers = _sql_code(text)
|
|
1841
|
+
issues = []
|
|
1842
|
+
for pattern, rule in _SQL_RULES:
|
|
1843
|
+
for match in re.finditer(pattern, code, re.IGNORECASE):
|
|
1844
|
+
# 外键 ON DELETE 引用动作不是删除数据语句;注释已置空,换行不影响判断。
|
|
1845
|
+
if rule == "CVE-KD-SQL-0007" and re.search(r"\bon\s*$", code[:match.start()], re.IGNORECASE):
|
|
1846
|
+
continue
|
|
1847
|
+
issues.append({"line": code.count("\n", 0, match.start()) + 1, "ruleCodes": [rule], "dcsRisk": RULES[rule][0],
|
|
1848
|
+
"dcsRule": RULES[rule][1], "level": BLOCK, "message": f"{RULES[rule][1]}: {text[match.start():match.end()]}",
|
|
1849
|
+
"fix": "keep scripts to schema creation the platform allows; move data changes into operations"})
|
|
1850
|
+
for match in re.finditer(r"\bcreate\s+table\s+(?:if\s+not\s+exists\s+)?([\w.]+)", code, re.IGNORECASE):
|
|
1851
|
+
table_offset = match.start(1) + match.group(1).rfind(".") + 1
|
|
1852
|
+
table = identifiers.get(table_offset, match.group(1).rsplit(".", 1)[-1])
|
|
1853
|
+
if len(table) > 24:
|
|
1854
|
+
issues.append({"line": code.count("\n", 0, match.start()) + 1, "ruleCodes": ["CVE-KD-SQL-0014"], "dcsRisk": "严重",
|
|
1855
|
+
"dcsRule": RULES["CVE-KD-SQL-0014"][1], "level": BLOCK,
|
|
1856
|
+
"message": f"table name {table} has {len(table)} characters", "fix": "shorten the table name to 24 characters or fewer"})
|
|
1857
|
+
for pattern, rule, level, fix in (
|
|
1858
|
+
(r"\binsert\s+into\s+[\w.]+\s+(?:values|select|default)\b", "CVE-KD-SQL-0017", BLOCK, "list INSERT target columns explicitly"),
|
|
1859
|
+
(r"\b(?:create\s+(?:unique\s+)?index|alter\s+index)\b", "CVE-KD-SQL-0011", ADVISE, "verify target-table size and index impact with the deployment owner"),
|
|
1860
|
+
(r"\balter\s+table\s+[\w.]+\s+add\s+(?!constraint\b|primary\b|unique\b|foreign\b|check\b)(?:column\s+)?\w+", "CVE-KD-SQL-0012", ADVISE, "verify target-table size and ADD COLUMN impact before deployment"),
|
|
1861
|
+
(r"\balter\s+table\s+[\w.]+\s+(?:alter|modify)\s+(?:column\s+)?\w+", "CVE-KD-SQL-0013", ADVISE, "verify target-table size and column migration impact before deployment"),
|
|
1862
|
+
):
|
|
1863
|
+
for match in re.finditer(pattern, code, re.IGNORECASE):
|
|
1864
|
+
issues.append({"line": code.count("\n", 0, match.start()) + 1, "ruleCodes": [rule], "dcsRisk": RULES[rule][0],
|
|
1865
|
+
"dcsRule": RULES[rule][1], "level": level, "message": RULES[rule][1], "fix": fix})
|
|
1866
|
+
for directive in _SQL_TOKEN_RE.finditer(text):
|
|
1867
|
+
if directive.group(0).lower() == "/*dialect*/":
|
|
1868
|
+
rule = "CVE-KD-SQL-0016"
|
|
1869
|
+
issues.append({"line": text.count("\n", 0, directive.start()) + 1, "ruleCodes": [rule], "dcsRisk": RULES[rule][0],
|
|
1870
|
+
"dcsRule": RULES[rule][1], "level": ADVISE, "message": RULES[rule][1],
|
|
1871
|
+
"fix": "ISV products must remove dialect SQL; verify the deployment/product context"})
|
|
1872
|
+
# KSQL 的删除字段过程含字面量参数,须在执行语句处解析,不能把字符串/注释当调用。
|
|
1873
|
+
for match in re.finditer(r"\bexec(?:ute)?\s+(?:[\w]+\.)?p_AlterColumn\b", code, re.IGNORECASE):
|
|
1874
|
+
end = code.find(";", match.end())
|
|
1875
|
+
statement = text[match.end():end if end >= 0 else len(text)]
|
|
1876
|
+
if re.match(r"\s*'[^']+'\s*,\s*'[^']+'\s*,\s*'NULL'\s*(?:,|$)", statement, re.IGNORECASE):
|
|
1877
|
+
rule = "CVE-KD-SQL-0008"
|
|
1878
|
+
issues.append({"line": code.count("\n", 0, match.start()) + 1, "ruleCodes": [rule], "dcsRisk": RULES[rule][0],
|
|
1879
|
+
"dcsRule": RULES[rule][1], "level": BLOCK, "message": "p_AlterColumn deletes a column", "fix": "do not drop fields in deployment SQL"})
|
|
1880
|
+
return issues
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
_SKIPPED_DIRS = {"build", "out", "target", ".gradle", ".idea", ".git", "node_modules", ".temp", "bin"}
|
|
1884
|
+
|
|
1885
|
+
|
|
1886
|
+
def scan_path(root: Path) -> dict:
|
|
1887
|
+
files = [root] if root.is_file() else sorted(
|
|
1888
|
+
path for path in root.rglob("*")
|
|
1889
|
+
if path.is_file() and not (_SKIPPED_DIRS & set(path.relative_to(root).parts[:-1]))
|
|
1890
|
+
)
|
|
1891
|
+
findings, advisories, scanned = [], [], 0
|
|
1892
|
+
class_names: dict[str, list[tuple[Path, int]]] = {}
|
|
1893
|
+
for path in files:
|
|
1894
|
+
suffix = path.suffix.lower()
|
|
1895
|
+
if suffix == ".java":
|
|
1896
|
+
scanned += 1
|
|
1897
|
+
try:
|
|
1898
|
+
text = path.read_bytes().decode("utf-8-sig")
|
|
1899
|
+
except UnicodeDecodeError:
|
|
1900
|
+
findings.append({"file": str(path), "line": 1, "ruleCodes": ["CVE-KD-0501"], "dcsRisk": "高危",
|
|
1901
|
+
"dcsRule": RULES["CVE-KD-0501"][1], "level": BLOCK,
|
|
1902
|
+
"message": "source is not UTF-8; the JDK 17 build reads sources as UTF-8", "fix": "save the file as UTF-8"})
|
|
1903
|
+
continue
|
|
1904
|
+
for issue in scan_source(text, path):
|
|
1905
|
+
(findings if issue["level"] == BLOCK else advisories).append({"file": str(path), **issue})
|
|
1906
|
+
source = _Source(text)
|
|
1907
|
+
package = re.search(r"\bpackage\s+([\w.]+)\s*;", source.code)
|
|
1908
|
+
for declared in re.finditer(r"\b(?:class|interface|enum|record)\s+([\w$]+)", source.code):
|
|
1909
|
+
if source.brace_depth(declared.start()) == 0:
|
|
1910
|
+
name = (package.group(1) + "." if package else "") + declared.group(1)
|
|
1911
|
+
class_names.setdefault(name, []).append((path, source.line(declared.start())))
|
|
1912
|
+
elif suffix == ".sql":
|
|
1913
|
+
scanned += 1
|
|
1914
|
+
for issue in scan_sql(path.read_bytes()):
|
|
1915
|
+
(findings if issue["level"] == BLOCK else advisories).append({"file": str(path), **issue})
|
|
1916
|
+
elif suffix in (".jar", ".class", ".so", ".dll", ".dylib"):
|
|
1917
|
+
rule = "CVE-KD-SCAN-jar" if suffix == ".jar" else "CVE-KD-SCAN-libFile"
|
|
1918
|
+
findings.append({"file": str(path), "line": 0, "ruleCodes": [rule], "dcsRisk": RULES[rule][0], "dcsRule": RULES[rule][1],
|
|
1919
|
+
"level": BLOCK, "message": f"packaged binary {path.name}",
|
|
1920
|
+
"fix": "remove it; depend only on the platform SDK libraries, which DCS provides at deployment"})
|
|
1921
|
+
for name, declarations in class_names.items():
|
|
1922
|
+
if len(declarations) < 2:
|
|
1923
|
+
continue
|
|
1924
|
+
rule = "class_fully_qualified_name_conflict_internal"
|
|
1925
|
+
for path, line in declarations:
|
|
1926
|
+
findings.append({"file": str(path), "line": line, "ruleCodes": [rule], "dcsRisk": RULES[rule][0],
|
|
1927
|
+
"dcsRule": RULES[rule][1], "level": BLOCK, "message": f"duplicate class {name} in the inspected source module",
|
|
1928
|
+
"fix": "remove duplicate source classes or use distinct fully qualified names"})
|
|
1929
|
+
return {"findings": findings, "advisories": advisories, "scanned": scanned}
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
def main() -> int:
|
|
1933
|
+
parser = argparse.ArgumentParser(description="Check Java plugin source against the DCS deployment gate.")
|
|
1934
|
+
parser.add_argument("--source", action="append", required=True, help="a .java/.sql file or a module directory; repeatable")
|
|
1935
|
+
parser.add_argument("--strict", action="store_true", help="also fail on advisory findings")
|
|
1936
|
+
args = parser.parse_args()
|
|
1937
|
+
findings, advisories, scanned, missing = [], [], 0, []
|
|
1938
|
+
scan_errors, empty_sources = [], []
|
|
1939
|
+
for item in args.source:
|
|
1940
|
+
path = Path(item).expanduser().resolve()
|
|
1941
|
+
if not path.exists():
|
|
1942
|
+
missing.append(str(path))
|
|
1943
|
+
continue
|
|
1944
|
+
try:
|
|
1945
|
+
result = scan_path(path)
|
|
1946
|
+
except OSError as exc:
|
|
1947
|
+
scan_errors.append({"file": str(path), "message": str(exc)})
|
|
1948
|
+
continue
|
|
1949
|
+
if not result["scanned"] and not result["findings"]:
|
|
1950
|
+
empty_sources.append(str(path))
|
|
1951
|
+
findings += result["findings"]
|
|
1952
|
+
advisories += result["advisories"]
|
|
1953
|
+
scanned += result["scanned"]
|
|
1954
|
+
status = not (missing or scan_errors or empty_sources or findings or (args.strict and advisories))
|
|
1955
|
+
print(json.dumps({
|
|
1956
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
1957
|
+
"status": status,
|
|
1958
|
+
"scannedFiles": scanned,
|
|
1959
|
+
"missing": missing,
|
|
1960
|
+
"scanErrors": scan_errors,
|
|
1961
|
+
"emptySources": empty_sources,
|
|
1962
|
+
"ruleSource": "金蝶AI套件公有云门禁20261026.xlsx ∪ 扫描规则_1026144356.xlsx(高危/严重)",
|
|
1963
|
+
"ruleSources": CATALOG["sources"],
|
|
1964
|
+
"coverage": {"cataloguedRuleCount": len(CATALOG["rules"]), "scope": "source-text", "complete": False},
|
|
1965
|
+
"catalogueMerge": {"overlapCount": CATALOG["overlapCount"], "addedCount": CATALOG["addedCount"]},
|
|
1966
|
+
"findingCount": len(findings),
|
|
1967
|
+
"advisoryCount": len(advisories),
|
|
1968
|
+
"findings": findings,
|
|
1969
|
+
"advisories": advisories,
|
|
1970
|
+
"notCovered": "final JAR/external class-name conflicts, metadata/KSC/TypeScript scans, null-flow, injection/ownership and other whole-build analyses stay with DCS; catalogue registration is not detector coverage",
|
|
1971
|
+
}, ensure_ascii=False, indent=2))
|
|
1972
|
+
return 0 if status else 2
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
if __name__ == "__main__":
|
|
1976
|
+
sys.exit(main())
|