For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 讓 PM 在 GRC 專案中填寫 SSP 控制項層級和 AO(檢查項目)層級的現況實作描述,並可附加關聯程序書文件。
Architecture: 擴展 jedi-oscal 套件的既有 ControlImplementation DDD 棧(Model → Mapper → Entity → Repo → DomainService),新增 control_identifier、control_origination、remarks 欄位和 SspControlImplementationObjective 全棧。主專案新增 SSP Route + App Service + Serializer。專案啟動流程加入 Step 7.5 初始化邏輯。
Tech Stack: Python 3.11, Flask-RESTful, SQLAlchemy 2.0 (mapped_column), dependency-injector, marshmallow, PostgreSQL (JSONB)
修改範圍總覽:
| 區域 | 套件 | 新增/修改 | 檔案數 |
|---|---|---|---|
| ORM Model | jedi-oscal | 修改 1 + 新增 1 | 2 |
| Entity | jedi-oscal | 修改 2 + 新增 2 | 4 |
| Mapper | jedi-oscal | 修改 1 + 新增 1 | 2 |
| Repo | jedi-oscal | 修改 1 + 新增 2 | 3 |
| Domain Service | jedi-oscal | 修改 1 + 新增 1 | 2 |
| App DTO | jedi-oscal | 修改 1 + 新增 1 | 2 |
| init exports | jedi-oscal | 修改 5 | 5 |
| Error Code | 主專案 | 修改 1 | 1 |
| Serializer | 主專案 | 修改 1 + 新增 1 | 2 |
| App Service | 主專案 | 新增 1 | 1 |
| Route | 主專案 | 新增 1 | 1 |
| DI Container | 主專案 | 修改 1 | 1 |
| Blueprint 註冊 | 主專案 | 修改 1 | 1 |
| 專案啟動流程 | 主專案 | 修改 1 | 1 |
| Migration SQL | 主專案 | 新增 1 | 1 |
| 合計 | ~30 |
~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/)修改:
infra/model/ssp/ssp_control_implementation.py — 加欄位 control_identifier, control_origination, remarks;catalog_control_id 改 nullable;新增 objectives relationshipinfra/mapper/ssp/control_implementation_mapper.py — 新增欄位映射domain/entity/ssp/ssp_control_implementation_entity.py — 新增欄位domain/entity/ssp/ssp_control_implementation_query_entity.py — 新增 control_identifier 查詢欄位app/dto/ssp/ssp_control_implementation_dto.py — 新增欄位 + objectives listdomain/services/ssp/control_implementation_domain_service.py — 新增 get_by_ssp_and_identifier 方法infra/repository/ssp/ssp_control_implementation_repo_impl.py — 新增自訂查詢方法(by ssp_id + control_identifier)新增:
infra/model/ssp/ssp_control_implementation_objective.py — ORM Modelinfra/mapper/ssp/control_implementation_objective_mapper.py — Mapperdomain/entity/ssp/ssp_control_impl_objective_entity.py — Entitydomain/entity/ssp/ssp_control_impl_objective_query_entity.py — Query Entitydomain/repository/ssp/control_implementation_objective_repo.py — IRepoinfra/repository/ssp/ssp_control_impl_objective_repo_impl.py — Repo Impldomain/services/ssp/control_implementation_objective_domain_service.py — Domain Serviceapp/dto/ssp/ssp_control_impl_objective_dto.py — DTOinit exports 修改:
infra/model/ssp/__init__.pyinfra/repository/ssp/__init__.pydomain/repository/ssp/__init__.pydomain/services/ssp/__init__.pydomain/entity/ssp/__init__.py(如有)compliance-manager-be/)修改:
common/code/grc_error_code.py — 新增 SSP 相關 error codeapi/oscal/serializers/ssp/ssp_control_implementation.py — 擴充欄位 + request schemaapi/oscal/__init__.py — 註冊 SSP routesdi_containers/oscal/oscal_containers.py — 註冊 objective repo/domain service/app serviceapp/project/service/oscal_project_service.py — Step 7.5 初始化新增:
api/oscal/serializers/ssp/ssp_control_impl_objective.py — Objective serializerapp/oscal/service/ssp_control_implementation_service.py — App Serviceapi/oscal/routes/ssp/ssp_control_implementation_route.py — Routescripts/sql/ssp_control_implementation_migration.sql — Migration SQLFiles:
compliance-manager-be/scripts/sql/ssp_control_implementation_migration.sql~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_control_implementation.py~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal/jedi_oscal/infra/model/ssp/ssp_control_implementation_objective.py-- SSP Control Implementation 欄位異動
-- 1) catalog_control_id 改為 nullable
ALTER TABLE oscal.system_security_plan_control_implementations
ALTER COLUMN catalog_control_id DROP NOT NULL;
-- 2) 新增欄位
ALTER TABLE oscal.system_security_plan_control_implementations
ADD COLUMN IF NOT EXISTS control_identifier VARCHAR(100),
ADD COLUMN IF NOT EXISTS control_origination VARCHAR(50) DEFAULT 'organization',
ADD COLUMN IF NOT EXISTS remarks TEXT;
-- 3) 補回填 control_identifier(從 catalog_controls 反查)
UPDATE oscal.system_security_plan_control_implementations ci
SET control_identifier = cc.control_id
FROM oscal.catalog_controls cc
WHERE ci.catalog_control_id = cc.id
AND ci.control_identifier IS NULL;
-- 4) control_identifier 設為 NOT NULL(回填完成後)
ALTER TABLE oscal.system_security_plan_control_implementations
ALTER COLUMN control_identifier SET NOT NULL;
-- 5) 新增 index
CREATE INDEX IF NOT EXISTS ix_ssp_ctrl_impl_control_identifier
ON oscal.system_security_plan_control_implementations (system_security_plan_id, control_identifier);
-- 6) 更新 unique constraint(改用 control_identifier 取代 catalog_control_id)
ALTER TABLE oscal.system_security_plan_control_implementations
DROP CONSTRAINT IF EXISTS uq_ssp_ctrl_impl_ssp_catalog_control;
ALTER TABLE oscal.system_security_plan_control_implementations
ADD CONSTRAINT uq_ssp_ctrl_impl_ssp_control_identifier
UNIQUE (system_security_plan_id, control_identifier);
-- ============================================================
-- SSP Control Implementation Objectives 新表
-- ============================================================
CREATE TABLE IF NOT EXISTS oscal.ssp_control_implementation_objectives (
id SERIAL PRIMARY KEY,
uid UUID NOT NULL DEFAULT gen_random_uuid(),
system_security_plan_id INTEGER NOT NULL
REFERENCES oscal.system_security_plans(id) ON DELETE CASCADE,
control_implementation_id INTEGER NOT NULL
REFERENCES oscal.system_security_plan_control_implementations(id)
ON DELETE CASCADE,
control_identifier VARCHAR(100) NOT NULL,
statement_identifier VARCHAR(100) NOT NULL,
implementation_status VARCHAR(50),
implementation_description TEXT,
remarks TEXT,
reference_documents JSONB DEFAULT '[]'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_user VARCHAR(100),
updated_user VARCHAR(100),
CONSTRAINT uq_ssp_obj_ctrl_impl_stmt
UNIQUE (control_implementation_id, statement_identifier)
);
CREATE INDEX IF NOT EXISTS ix_ssp_obj_uid
ON oscal.ssp_control_implementation_objectives (uid);
CREATE INDEX IF NOT EXISTS ix_ssp_obj_ctrl_impl_id
ON oscal.ssp_control_implementation_objectives (control_implementation_id);
CREATE INDEX IF NOT EXISTS ix_ssp_obj_ssp_ctrl_id
ON oscal.ssp_control_implementation_objectives (system_security_plan_id, control_identifier);寫入 scripts/sql/ssp_control_implementation_migration.sql。
在 OscalSystemSecurityPlanControlImplementation 類別中:
catalog_control_id 的 nullable 改為 TrueUniqueConstraint("system_security_plan_id", "catalog_control_id", ...)UniqueConstraint("system_security_plan_id", "control_identifier", name="uq_ssp_ctrl_impl_ssp_control_identifier")control_identifier: Mapped[str] = mapped_column(
String(100),
nullable=False,
comment="控制項編號,如 AC.L1-3.1.1",
)
control_origination: Mapped[Optional[str]] = mapped_column(
String(50),
default="organization",
comment="organization | system-specific | customer-configured | inherited | shared",
)
remarks: Mapped[Optional[str]] = mapped_column(
Text,
comment="補充備註",
)objectives: Mapped[list["OscalSspControlImplementationObjective"]] = relationship(
"OscalSspControlImplementationObjective",
back_populates="control_implementation",
cascade="all, delete-orphan",
lazy="select",
)Index("ix_ssp_ctrl_impl_control_identifier", "system_security_plan_id", "control_identifier")OscalSspControlImplementationObjectiveimport uuid
from typing import Optional, TYPE_CHECKING
from datetime import datetime
from sqlalchemy import (
String, Text, UUID, ForeignKey, DateTime, func,
Index, UniqueConstraint, Integer,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from jedi_common.session.database.declarative_base import Base
if TYPE_CHECKING:
from jedi_oscal.infra.model.ssp.ssp import OscalSystemSecurityPlan
from jedi_oscal.infra.model.ssp.ssp_control_implementation import (
OscalSystemSecurityPlanControlImplementation,
)
class OscalSspControlImplementationObjective(Base):
"""SSP Control Implementation Objective (AO 層級)"""
__tablename__ = "ssp_control_implementation_objectives"
__table_args__ = (
UniqueConstraint(
"control_implementation_id", "statement_identifier",
name="uq_ssp_obj_ctrl_impl_stmt",
),
Index("ix_ssp_obj_uid", "uid"),
Index("ix_ssp_obj_ctrl_impl_id", "control_implementation_id"),
Index("ix_ssp_obj_ssp_ctrl_id", "system_security_plan_id", "control_identifier"),
{"schema": "oscal"},
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
uid: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), default=uuid.uuid4, unique=True, nullable=False,
)
system_security_plan_id: Mapped[int] = mapped_column(
ForeignKey("oscal.system_security_plans.id", ondelete="CASCADE"),
nullable=False,
)
control_implementation_id: Mapped[int] = mapped_column(
ForeignKey("oscal.system_security_plan_control_implementations.id", ondelete="CASCADE"),
nullable=False,
)
control_identifier: Mapped[str] = mapped_column(String(100), nullable=False)
statement_identifier: Mapped[str] = mapped_column(String(100), nullable=False)
implementation_status: Mapped[Optional[str]] = mapped_column(String(50))
implementation_description: Mapped[Optional[str]] = mapped_column(Text)
remarks: Mapped[Optional[str]] = mapped_column(Text)
reference_documents: Mapped[Optional[list]] = mapped_column(
JSONB, default=list, server_default="'[]'::jsonb",
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=func.now(), nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False,
)
created_user: Mapped[Optional[str]] = mapped_column(String(100))
updated_user: Mapped[Optional[str]] = mapped_column(String(100))
# Relationships
control_implementation: Mapped["OscalSystemSecurityPlanControlImplementation"] = relationship(
"OscalSystemSecurityPlanControlImplementation",
back_populates="objectives",
)
system_security_plan: Mapped["OscalSystemSecurityPlan"] = relationship(
"OscalSystemSecurityPlan",
)from .ssp_control_implementation_objective import OscalSspControlImplementationObjectiveRun: cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal && python -c "from jedi_oscal.infra.model.ssp.ssp_control_implementation_objective import OscalSspControlImplementationObjective; print('OK')"
Files:
jedi_oscal/domain/entity/ssp/ssp_control_implementation_entity.pyjedi_oscal/domain/entity/ssp/ssp_control_implementation_query_entity.pyjedi_oscal/domain/entity/ssp/ssp_control_impl_objective_entity.pyjedi_oscal/domain/entity/ssp/ssp_control_impl_objective_query_entity.py新增 __init__ 參數與屬性:
control_identifier: Optional[str] = None,
control_origination: Optional[str] = None,
remarks: Optional[str] = None,
objectives: Optional[list] = None, # list["ControlImplementationObjectiveEntity"]在 __init__ body 中:
self.control_identifier = control_identifier
self.control_origination = control_origination
self.remarks = remarks
self.objectives = objectives or []新增 control_identifier: Optional[str] = None 參數與屬性。
from datetime import datetime
from typing import Optional
import uuid
class ControlImplementationObjectiveEntity:
"""SSP Control Implementation Objective domain entity."""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[uuid.UUID] = None,
system_security_plan_id: Optional[int] = None,
control_implementation_id: Optional[int] = None,
control_identifier: Optional[str] = None,
statement_identifier: Optional[str] = None,
implementation_status: Optional[str] = None,
implementation_description: Optional[str] = None,
remarks: Optional[str] = None,
reference_documents: Optional[list] = None,
created_user: Optional[str] = None,
updated_user: Optional[str] = None,
created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None,
):
self.id = id
self.uid = uid
self.system_security_plan_id = system_security_plan_id
self.control_implementation_id = control_implementation_id
self.control_identifier = control_identifier
self.statement_identifier = statement_identifier
self.implementation_status = implementation_status
self.implementation_description = implementation_description
self.remarks = remarks
self.reference_documents = reference_documents or []
self.created_user = created_user
self.updated_user = updated_user
self.created_at = created_at
self.updated_at = updated_atfrom datetime import datetime
from typing import Optional
import uuid
class SspControlImplObjectiveQueryEntity:
"""Query container for filtering ControlImplementationObjective."""
def __init__(
self,
id: Optional[int] = None,
uid: Optional[uuid.UUID] = None,
system_security_plan_id: Optional[int] = None,
control_implementation_id: Optional[int] = None,
control_identifier: Optional[str] = None,
statement_identifier: Optional[str] = None,
implementation_status: Optional[str] = None,
):
self.id = id
self.uid = uid
self.system_security_plan_id = system_security_plan_id
self.control_implementation_id = control_implementation_id
self.control_identifier = control_identifier
self.statement_identifier = statement_identifier
self.implementation_status = implementation_statusFiles:
jedi_oscal/infra/mapper/ssp/control_implementation_mapper.pyjedi_oscal/infra/mapper/ssp/control_implementation_objective_mapper.py在 to_entity() 中新增:
control_identifier=getattr(model, "control_identifier", None),
control_origination=getattr(model, "control_origination", None),
remarks=getattr(model, "remarks", None),
objectives=ControlImplementationObjectiveMapper.to_list_entity(
getattr(model, "objectives", []) or []
),在 to_model() 中新增:
m.control_identifier = getattr(entity, "control_identifier", None)
m.control_origination = getattr(entity, "control_origination", None)
m.remarks = getattr(entity, "remarks", None)新增 import:
from jedi_oscal.infra.mapper.ssp.control_implementation_objective_mapper import ControlImplementationObjectiveMapperfrom typing import List, Optional
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_entity import ControlImplementationObjectiveEntity
from jedi_oscal.infra.model.ssp.ssp_control_implementation_objective import OscalSspControlImplementationObjective
class ControlImplementationObjectiveMapper:
@staticmethod
def to_entity(model: OscalSspControlImplementationObjective) -> ControlImplementationObjectiveEntity:
return ControlImplementationObjectiveEntity(
id=getattr(model, "id", None),
uid=getattr(model, "uid", None),
system_security_plan_id=getattr(model, "system_security_plan_id", None),
control_implementation_id=getattr(model, "control_implementation_id", None),
control_identifier=getattr(model, "control_identifier", None),
statement_identifier=getattr(model, "statement_identifier", None),
implementation_status=getattr(model, "implementation_status", None),
implementation_description=getattr(model, "implementation_description", None),
remarks=getattr(model, "remarks", None),
reference_documents=getattr(model, "reference_documents", None) or [],
created_user=getattr(model, "created_user", None),
updated_user=getattr(model, "updated_user", None),
created_at=getattr(model, "created_at", None),
updated_at=getattr(model, "updated_at", None),
)
@staticmethod
def to_list_entity(models: list) -> list[ControlImplementationObjectiveEntity]:
return [ControlImplementationObjectiveMapper.to_entity(m) for m in models]
@staticmethod
def to_model(
entity: ControlImplementationObjectiveEntity,
model: Optional[OscalSspControlImplementationObjective] = None,
) -> OscalSspControlImplementationObjective:
m = model or OscalSspControlImplementationObjective()
if getattr(entity, "uid", None) is not None:
m.uid = entity.uid
m.system_security_plan_id = entity.system_security_plan_id
m.control_implementation_id = entity.control_implementation_id
m.control_identifier = entity.control_identifier
m.statement_identifier = entity.statement_identifier
m.implementation_status = getattr(entity, "implementation_status", None)
m.implementation_description = getattr(entity, "implementation_description", None)
m.remarks = getattr(entity, "remarks", None)
m.reference_documents = getattr(entity, "reference_documents", None) or []
if hasattr(m, "created_user"):
m.created_user = getattr(entity, "created_user", None)
if hasattr(m, "updated_user"):
m.updated_user = getattr(entity, "updated_user", None)
return m
@staticmethod
def to_list_model(entities: list) -> list[OscalSspControlImplementationObjective]:
return [ControlImplementationObjectiveMapper.to_model(e) for e in entities]Files:
jedi_oscal/domain/repository/ssp/control_implementation_objective_repo.pyjedi_oscal/infra/repository/ssp/ssp_control_impl_objective_repo_impl.pyjedi_oscal/infra/repository/ssp/ssp_control_implementation_repo_impl.pyjedi_oscal/infra/repository/ssp/__init__.pyjedi_oscal/domain/repository/ssp/__init__.pyfrom jedi_common.session.database.repository.base_repository import IBaseRepo
class IControlImplementationObjectiveRepo(IBaseRepo):
passfrom jedi_common.session.database.repository.base_repository_impl import BaseRepositoryImpl
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_entity import ControlImplementationObjectiveEntity
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_query_entity import SspControlImplObjectiveQueryEntity
from jedi_oscal.domain.repository.ssp.control_implementation_objective_repo import IControlImplementationObjectiveRepo
from jedi_oscal.infra.mapper.ssp.control_implementation_objective_mapper import ControlImplementationObjectiveMapper
from jedi_oscal.infra.model.ssp.ssp_control_implementation_objective import OscalSspControlImplementationObjective
class ControlImplementationObjectiveRepoImpl(
IControlImplementationObjectiveRepo[ControlImplementationObjectiveEntity, SspControlImplObjectiveQueryEntity],
BaseRepositoryImpl[
ControlImplementationObjectiveEntity,
SspControlImplObjectiveQueryEntity,
OscalSspControlImplementationObjective,
ControlImplementationObjectiveMapper,
],
):
def __init__(self):
super().__init__(
mapper=ControlImplementationObjectiveMapper,
model=OscalSspControlImplementationObjective,
)在 ControlImplementationRepoImpl 中新增自訂方法,以支援 ssp_id + control_identifier 的查詢:
from jedi_oscal.infra.model.ssp.ssp_control_implementation import OscalSystemSecurityPlanControlImplementation
def get_by_ssp_and_identifier(self, ssp_id: int, control_identifier: str):
"""依 ssp_id + control_identifier 取得單筆,含 objectives"""
model = self.session.query(OscalSystemSecurityPlanControlImplementation).filter(
OscalSystemSecurityPlanControlImplementation.system_security_plan_id == ssp_id,
OscalSystemSecurityPlanControlImplementation.control_identifier == control_identifier,
).first()
if not model:
return None
return self.mapper.to_entity(model)
def get_all_by_ssp_id(self, ssp_id: int):
"""取得某 SSP 下所有 control implementations"""
models = self.session.query(OscalSystemSecurityPlanControlImplementation).filter(
OscalSystemSecurityPlanControlImplementation.system_security_plan_id == ssp_id,
).all()
return self.mapper.to_list_entity(models)同時在 IControlImplementationRepo 介面加上這兩個 abstract method:
from abc import abstractmethod
@abstractmethod
def get_by_ssp_and_identifier(self, ssp_id: int, control_identifier: str):
pass
@abstractmethod
def get_all_by_ssp_id(self, ssp_id: int):
passinfra/repository/ssp/__init__.py:
from .ssp_control_impl_objective_repo_impl import ControlImplementationObjectiveRepoImpldomain/repository/ssp/__init__.py:
from .control_implementation_objective_repo import IControlImplementationObjectiveRepoFiles:
jedi_oscal/domain/services/ssp/control_implementation_domain_service.pyjedi_oscal/domain/services/ssp/control_implementation_objective_domain_service.pyjedi_oscal/domain/services/ssp/__init__.py新增方法:
def get_by_ssp_and_identifier(self, ssp_id: int, control_identifier: str):
return self.control_implementation_repo.get_by_ssp_and_identifier(ssp_id, control_identifier)
def get_all_by_ssp_id(self, ssp_id: int):
return self.control_implementation_repo.get_all_by_ssp_id(ssp_id)from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_entity import ControlImplementationObjectiveEntity
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_query_entity import SspControlImplObjectiveQueryEntity
from jedi_oscal.domain.repository.ssp import IControlImplementationObjectiveRepo
class ControlImplementationObjectiveDomainService:
def __init__(self, repo: IControlImplementationObjectiveRepo):
self.repo = repo
def get_all(self, _filter: SspControlImplObjectiveQueryEntity = None):
return self.repo.get_all_by_fields(_filter)
def get_one(self, _filter: SspControlImplObjectiveQueryEntity):
return self.repo.get_one_by_fields(_filter)
def get_by_uid(self, uid: str):
return self.repo.get_by_uid(uid)
def get_by_id(self, id: int):
return self.repo.get_by_id(id)
def add(self, entity: ControlImplementationObjectiveEntity):
return self.repo.add(entity)
def update(self, entity: ControlImplementationObjectiveEntity):
return self.repo.update(entity)
def delete_by_id(self, id: int):
return self.repo.delete_by_id(id)
def delete_by_uid(self, uid: str):
return self.repo.delete_by_uid(uid)from .control_implementation_objective_domain_service import ControlImplementationObjectiveDomainServiceFiles:
jedi_oscal/app/dto/ssp/ssp_control_implementation_dto.pyjedi_oscal/app/dto/ssp/ssp_control_impl_objective_dto.pyfrom __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional
from jedi_oscal.app.dto.base.oscal_base_dto import OscalBaseDTO
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_entity import ControlImplementationObjectiveEntity
@dataclass
class ControlImplementationObjectiveDTO(OscalBaseDTO):
uid: Optional[str] = None
system_security_plan_id: Optional[int] = None
control_implementation_id: Optional[int] = None
control_identifier: Optional[str] = None
statement_identifier: Optional[str] = None
implementation_status: Optional[str] = None
implementation_description: Optional[str] = None
remarks: Optional[str] = None
reference_documents: list = field(default_factory=list)
@staticmethod
def from_entity(entity: ControlImplementationObjectiveEntity) -> "ControlImplementationObjectiveDTO":
return ControlImplementationObjectiveDTO(
id=getattr(entity, "id", None),
uid=str(entity.uid) if getattr(entity, "uid", None) else None,
system_security_plan_id=getattr(entity, "system_security_plan_id", None),
control_implementation_id=getattr(entity, "control_implementation_id", None),
control_identifier=getattr(entity, "control_identifier", None),
statement_identifier=getattr(entity, "statement_identifier", None),
implementation_status=getattr(entity, "implementation_status", None),
implementation_description=getattr(entity, "implementation_description", None),
remarks=getattr(entity, "remarks", None),
reference_documents=getattr(entity, "reference_documents", None) or [],
created_at=getattr(entity, "created_at", None),
updated_at=getattr(entity, "updated_at", None),
created_user=getattr(entity, "created_user", None),
updated_user=getattr(entity, "updated_user", None),
)
@staticmethod
def from_entity_list(entities: list) -> list["ControlImplementationObjectiveDTO"]:
return [ControlImplementationObjectiveDTO.from_entity(e) for e in entities]新增欄位:
control_identifier: Optional[str] = None
control_origination: Optional[str] = None
remarks: Optional[str] = None
objectives: list = field(default_factory=list) # list[ControlImplementationObjectiveDTO]在 from_entity() 中新增:
from jedi_oscal.app.dto.ssp.ssp_control_impl_objective_dto import ControlImplementationObjectiveDTO
control_identifier=getattr(entity, "control_identifier", None),
control_origination=getattr(entity, "control_origination", None),
remarks=getattr(entity, "remarks", None),
objectives=ControlImplementationObjectiveDTO.from_entity_list(
getattr(entity, "objectives", []) or []
),在 ControlImplementationDTO 上方加 from dataclasses import dataclass, field(取代原有 dataclass import)。
Files:
compliance-manager-be/common/code/grc_error_code.pycompliance-manager-be/api/oscal/serializers/ssp/ssp_control_implementation.pycompliance-manager-be/api/oscal/serializers/ssp/ssp_control_impl_objective.py在 GrcErrorCode 中新增:
# ── SSP Control Implementation ──────────────────────────────────────
GRC_SSP_NOT_FOUND = ("SSP 系統安全計畫不存在", "GRC_404012")
GRC_SSP_CONTROL_IMPL_NOT_FOUND = ("SSP 控制項實作記錄不存在", "GRC_404013")
GRC_SSP_OBJECTIVE_NOT_FOUND = ("SSP 檢查項目記錄不存在", "GRC_404014")from marshmallow import Schema, fields
class ObjectiveResponseSchema(Schema):
uid = fields.Str()
statement_identifier = fields.Str()
implementation_status = fields.Str(allow_none=True)
implementation_description = fields.Str(allow_none=True)
remarks = fields.Str(allow_none=True)
reference_documents = fields.List(fields.Dict(), load_default=[])
created_at = fields.DateTime()
updated_at = fields.DateTime()
class ObjectiveUpdateRequestSchema(Schema):
implementation_status = fields.Str(load_default=None, allow_none=True)
implementation_description = fields.Str(load_default=None, allow_none=True)
remarks = fields.Str(load_default=None, allow_none=True)
reference_documents = fields.List(fields.Dict(), load_default=None, allow_none=True)新增欄位:
control_identifier = fields.Str()
control_origination = fields.Str(allow_none=True)
remarks = fields.Str(allow_none=True)
from api.oscal.serializers.ssp.ssp_control_impl_objective import ObjectiveResponseSchema
# ...(放在檔案頂部 import 區)
objectives = fields.List(fields.Nested(ObjectiveResponseSchema), dump_default=[])新增 Request Schema(同一檔案底部):
class ControlImplementationUpdateRequestSchema(Schema):
implementation_status = fields.Str(load_default=None, allow_none=True)
implementation_description = fields.Str(load_default=None, allow_none=True)
remarks = fields.Str(load_default=None, allow_none=True)
class ControlImplementationListItemSchema(Schema):
control_identifier = fields.Str()
implementation_status = fields.Str(allow_none=True)
implementation_description = fields.Str(allow_none=True)
objective_count = fields.Int()
objective_described_count = fields.Int()
has_description = fields.Bool()
class ControlImplementationListSummarySchema(Schema):
total = fields.Int()
described = fields.Int()
not_described = fields.Int()
completion_rate = fields.Float()
class ControlImplementationListResponseSchema(Schema):
items = fields.List(fields.Nested(ControlImplementationListItemSchema))
summary = fields.Nested(ControlImplementationListSummarySchema)Files:
compliance-manager-be/app/oscal/service/ssp_control_implementation_service.pyfrom typing import Optional
from jedi_common.session.database.db import transaction
from jedi_common.handler.exception import NotFound
from common.code.grc_error_code import GrcErrorCode
from jedi_oscal.domain.services.ssp.control_implementation_domain_service import ControlImplementationDomainService
from jedi_oscal.domain.services.ssp.control_implementation_objective_domain_service import ControlImplementationObjectiveDomainService
from jedi_oscal.domain.services.ssp.ssp_domain_service import SystemSecurityPlanDomainService
from jedi_oscal.domain.entity.ssp.ssp_control_implementation_entity import ControlImplementationEntity
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_entity import ControlImplementationObjectiveEntity
from jedi_oscal.domain.entity.ssp.ssp_control_impl_objective_query_entity import SspControlImplObjectiveQueryEntity
from jedi_oscal.app.dto.ssp.ssp_control_implementation_dto import ControlImplementationDTO
from jedi_oscal.app.dto.ssp.ssp_control_impl_objective_dto import ControlImplementationObjectiveDTO
class SspControlImplementationService:
def __init__(
self,
ssp_domain_service: SystemSecurityPlanDomainService,
control_impl_domain_service: ControlImplementationDomainService,
objective_domain_service: ControlImplementationObjectiveDomainService,
):
self._ssp_ds = ssp_domain_service
self._ctrl_ds = control_impl_domain_service
self._obj_ds = objective_domain_service
def _get_ssp_or_404(self, ssp_uid: str):
ssp = self._ssp_ds.get_by_uid(ssp_uid)
if not ssp:
raise NotFound(GrcErrorCode.GRC_SSP_NOT_FOUND)
return ssp
def _get_or_create_control_impl(self, ssp_id: int, control_identifier: str, curr_user: str):
"""取得或自動建立 control implementation"""
entity = self._ctrl_ds.get_by_ssp_and_identifier(ssp_id, control_identifier)
if entity:
return entity
new_entity = ControlImplementationEntity(
system_security_plan_id=ssp_id,
control_identifier=control_identifier,
implementation_status="unknown", # 不可為 None,ORM 欄位 nullable=False
created_user=curr_user,
updated_user=curr_user,
)
return self._ctrl_ds.add(new_entity)
@transaction
def get_control_implementation(self, ssp_uid: str, control_identifier: str):
"""API 2.1: 取得控制項的 SSP 現況說明(含 objectives)"""
ssp = self._get_ssp_or_404(ssp_uid)
entity = self._ctrl_ds.get_by_ssp_and_identifier(ssp.id, control_identifier)
if not entity:
raise NotFound(GrcErrorCode.GRC_SSP_CONTROL_IMPL_NOT_FOUND)
return ControlImplementationDTO.from_entity(entity)
@transaction
def update_control_implementation(self, ssp_uid: str, control_identifier: str, curr_user: str, **kwargs):
"""API 2.2: 更新控制項層級的 SSP 現況說明(upsert)"""
ssp = self._get_ssp_or_404(ssp_uid)
entity = self._get_or_create_control_impl(ssp.id, control_identifier, curr_user)
if "implementation_status" in kwargs:
entity.implementation_status = kwargs["implementation_status"]
if "implementation_description" in kwargs:
entity.implementation_description = kwargs["implementation_description"]
if "remarks" in kwargs:
entity.remarks = kwargs["remarks"]
entity.updated_user = curr_user
updated = self._ctrl_ds.update(entity)
return ControlImplementationDTO.from_entity(updated)
@transaction
def update_objective(self, ssp_uid: str, control_identifier: str, statement_identifier: str, curr_user: str, **kwargs):
"""API 2.3: 更新 AO 層級的 SSP 現況說明(upsert)"""
ssp = self._get_ssp_or_404(ssp_uid)
ctrl = self._get_or_create_control_impl(ssp.id, control_identifier, curr_user)
# 查詢是否已存在
query = SspControlImplObjectiveQueryEntity(
control_implementation_id=ctrl.id,
statement_identifier=statement_identifier,
)
obj_entity = self._obj_ds.get_one(query)
if obj_entity:
if "implementation_status" in kwargs:
obj_entity.implementation_status = kwargs["implementation_status"]
if "implementation_description" in kwargs:
obj_entity.implementation_description = kwargs["implementation_description"]
if "remarks" in kwargs:
obj_entity.remarks = kwargs["remarks"]
if "reference_documents" in kwargs and kwargs["reference_documents"] is not None:
obj_entity.reference_documents = kwargs["reference_documents"]
obj_entity.updated_user = curr_user
updated = self._obj_ds.update(obj_entity)
else:
new_obj = ControlImplementationObjectiveEntity(
system_security_plan_id=ssp.id,
control_implementation_id=ctrl.id,
control_identifier=control_identifier,
statement_identifier=statement_identifier,
implementation_status=kwargs.get("implementation_status"),
implementation_description=kwargs.get("implementation_description"),
remarks=kwargs.get("remarks"),
reference_documents=kwargs.get("reference_documents") or [],
created_user=curr_user,
updated_user=curr_user,
)
updated = self._obj_ds.add(new_obj)
return ControlImplementationObjectiveDTO.from_entity(updated)
@transaction
def add_objective_document(self, ssp_uid: str, control_identifier: str, statement_identifier: str, file_ref: dict, curr_user: str):
"""API 2.4: 新增程序書關聯到 objective 的 reference_documents"""
ssp = self._get_ssp_or_404(ssp_uid)
ctrl = self._get_or_create_control_impl(ssp.id, control_identifier, curr_user)
query = SspControlImplObjectiveQueryEntity(
control_implementation_id=ctrl.id,
statement_identifier=statement_identifier,
)
obj_entity = self._obj_ds.get_one(query)
if not obj_entity:
raise NotFound(GrcErrorCode.GRC_SSP_OBJECTIVE_NOT_FOUND)
docs = obj_entity.reference_documents or []
docs.append(file_ref)
obj_entity.reference_documents = docs
obj_entity.updated_user = curr_user
updated = self._obj_ds.update(obj_entity)
return ControlImplementationObjectiveDTO.from_entity(updated)
@transaction
def remove_objective_document(self, ssp_uid: str, control_identifier: str, statement_identifier: str, file_id: str, curr_user: str):
"""API 2.5: 移除程序書關聯"""
ssp = self._get_ssp_or_404(ssp_uid)
ctrl = self._ctrl_ds.get_by_ssp_and_identifier(ssp.id, control_identifier)
if not ctrl:
raise NotFound(GrcErrorCode.GRC_SSP_CONTROL_IMPL_NOT_FOUND)
query = SspControlImplObjectiveQueryEntity(
control_implementation_id=ctrl.id,
statement_identifier=statement_identifier,
)
obj_entity = self._obj_ds.get_one(query)
if not obj_entity:
raise NotFound(GrcErrorCode.GRC_SSP_OBJECTIVE_NOT_FOUND)
docs = obj_entity.reference_documents or []
obj_entity.reference_documents = [d for d in docs if d.get("file_id") != file_id]
obj_entity.updated_user = curr_user
self._obj_ds.update(obj_entity)
@transaction
def list_control_implementations(self, ssp_uid: str, control_identifiers: Optional[list] = None, status: Optional[str] = None, has_description: Optional[bool] = None):
"""API 2.6: 批次取得多個控制項的 SSP 現況"""
ssp = self._get_ssp_or_404(ssp_uid)
all_ctrls = self._ctrl_ds.get_all_by_ssp_id(ssp.id)
# 篩選
if control_identifiers:
all_ctrls = [c for c in all_ctrls if c.control_identifier in control_identifiers]
if status:
all_ctrls = [c for c in all_ctrls if c.implementation_status == status]
items = []
described_count = 0
for ctrl in all_ctrls:
obj_count = len(ctrl.objectives) if ctrl.objectives else 0
obj_described = sum(
1 for o in (ctrl.objectives or [])
if o.implementation_description
)
has_desc = bool(ctrl.implementation_description)
if has_description is not None and has_desc != has_description:
continue
items.append({
"control_identifier": ctrl.control_identifier,
"implementation_status": ctrl.implementation_status,
"implementation_description": ctrl.implementation_description,
"objective_count": obj_count,
"objective_described_count": obj_described,
"has_description": has_desc,
})
if has_desc:
described_count += 1
total = len(items)
return {
"items": items,
"summary": {
"total": total,
"described": described_count,
"not_described": total - described_count,
"completion_rate": round(described_count / total, 2) if total > 0 else 0,
},
}Files:
compliance-manager-be/api/oscal/routes/ssp/ssp_control_implementation_route.pycompliance-manager-be/di_containers/oscal/oscal_containers.pycompliance-manager-be/api/oscal/__init__.pyfrom dependency_injector.wiring import inject, Provide
from flask import request
from flask_apispec import MethodResource, doc, use_kwargs, marshal_with
from flask_jwt_extended import jwt_required
from jedi_common.session.auth.auth_context import get_user_context
from api.oscal.serializers.ssp.ssp_control_implementation import (
ControlImplementationResponseSchema,
ControlImplementationUpdateRequestSchema,
ControlImplementationListResponseSchema,
)
from api.oscal.serializers.ssp.ssp_control_impl_objective import (
ObjectiveResponseSchema,
ObjectiveUpdateRequestSchema,
)
from app.oscal.service.ssp_control_implementation_service import SspControlImplementationService
from common.util.response_util import return_response
from di_containers.containers import Containers
import logging
logger = logging.getLogger(__name__)
class SspControlImplementationDetailRoute(MethodResource):
"""GET / PUT /ssp/<ssp_uid>/control-implementation/<control_identifier>"""
@doc(description="取得控制項的 SSP 現況說明", tags=["SSP"])
@marshal_with(ControlImplementationResponseSchema, apply=False)
@jwt_required()
@inject
def get(
self,
ssp_uid: str,
control_identifier: str,
ssp_ctrl_service: SspControlImplementationService = Provide[
Containers.oscal_container.ssp_control_implementation_service
],
):
result = ssp_ctrl_service.get_control_implementation(ssp_uid, control_identifier)
return return_response(True, ControlImplementationResponseSchema().dump(result))
@doc(description="更新控制項層級的 SSP 現況說明", tags=["SSP"])
@use_kwargs(ControlImplementationUpdateRequestSchema, location="json", apply=False)
@jwt_required()
@inject
def put(
self,
ssp_uid: str,
control_identifier: str,
ssp_ctrl_service: SspControlImplementationService = Provide[
Containers.oscal_container.ssp_control_implementation_service
],
):
user = get_user_context()
payload = request.get_json(silent=True) or {}
data = ControlImplementationUpdateRequestSchema().load(payload)
result = ssp_ctrl_service.update_control_implementation(
ssp_uid, control_identifier, user.login_name, **data
)
return return_response(True, ControlImplementationResponseSchema().dump(result))
class SspControlImplementationListRoute(MethodResource):
"""GET /ssp/<ssp_uid>/control-implementations"""
@doc(description="批次取得多個控制項的 SSP 現況", tags=["SSP"])
@marshal_with(ControlImplementationListResponseSchema, apply=False)
@jwt_required()
@inject
def get(
self,
ssp_uid: str,
ssp_ctrl_service: SspControlImplementationService = Provide[
Containers.oscal_container.ssp_control_implementation_service
],
):
control_ids_str = request.args.get("control_identifiers")
control_ids = control_ids_str.split(",") if control_ids_str else None
status = request.args.get("status")
has_desc_str = request.args.get("has_description")
has_description = None
if has_desc_str is not None:
has_description = has_desc_str.lower() == "true"
result = ssp_ctrl_service.list_control_implementations(
ssp_uid,
control_identifiers=control_ids,
status=status,
has_description=has_description,
)
return return_response(True, ControlImplementationListResponseSchema().dump(result))
class SspObjectiveDetailRoute(MethodResource):
"""PUT /ssp/<ssp_uid>/control-implementation/<control_identifier>/objective/<statement_identifier>"""
@doc(description="更新 AO 層級的 SSP 現況說明", tags=["SSP"])
@use_kwargs(ObjectiveUpdateRequestSchema, location="json", apply=False)
@jwt_required()
@inject
def put(
self,
ssp_uid: str,
control_identifier: str,
statement_identifier: str,
ssp_ctrl_service: SspControlImplementationService = Provide[
Containers.oscal_container.ssp_control_implementation_service
],
):
user = get_user_context()
payload = request.get_json(silent=True) or {}
data = ObjectiveUpdateRequestSchema().load(payload)
result = ssp_ctrl_service.update_objective(
ssp_uid, control_identifier, statement_identifier, user.login_name, **data
)
return return_response(True, ObjectiveResponseSchema().dump(result))
class SspObjectiveDocumentRoute(MethodResource):
"""DELETE /ssp/<ssp_uid>/control-implementation/<ci>/objective/<si>/document/<file_id>"""
@doc(description="移除程序書關聯", tags=["SSP"])
@jwt_required()
@inject
def delete(
self,
ssp_uid: str,
control_identifier: str,
statement_identifier: str,
file_id: str,
ssp_ctrl_service: SspControlImplementationService = Provide[
Containers.oscal_container.ssp_control_implementation_service
],
):
user = get_user_context()
ssp_ctrl_service.remove_objective_document(
ssp_uid, control_identifier, statement_identifier, file_id, user.login_name
)
return return_response(True, {})Note: 文件上傳 API(2.4)需整合
jedi-file-upload(ManagedFileUploadService),實作較複雜,建議在 Route 層處理request.files→ 上傳 → 取得 file_ref dict → 呼叫add_objective_document()。如需此功能可在後續 Task 擴充。
新增 imports:
from jedi_oscal.infra.repository.ssp import ControlImplementationRepoImpl, ControlImplementationObjectiveRepoImpl
from jedi_oscal.domain.services.ssp import ControlImplementationDomainService, ControlImplementationObjectiveDomainService
from app.oscal.service.ssp_control_implementation_service import SspControlImplementationService在 OscalContainer 中新增:
# SSP Control Implementation
control_implementation_repo = providers.Singleton(ControlImplementationRepoImpl)
control_implementation_objective_repo = providers.Singleton(ControlImplementationObjectiveRepoImpl)
control_implementation_domain_service = providers.Factory(
ControlImplementationDomainService,
control_implementation_repo=control_implementation_repo,
)
control_implementation_objective_domain_service = providers.Factory(
ControlImplementationObjectiveDomainService,
repo=control_implementation_objective_repo,
)
ssp_control_implementation_service = providers.Factory(
SspControlImplementationService,
ssp_domain_service=ssp_domain_service,
control_impl_domain_service=control_implementation_domain_service,
objective_domain_service=control_implementation_objective_domain_service,
)在 create_module() 中新增 imports 與路由註冊:
from api.oscal.routes.ssp.ssp_control_implementation_route import (
SspControlImplementationDetailRoute,
SspControlImplementationListRoute,
SspObjectiveDetailRoute,
SspObjectiveDocumentRoute,
)
# SSP Control Implementation
api.add_resource(
SspControlImplementationListRoute,
'/ssp/<string:ssp_uid>/control-implementations',
)
api.add_resource(
SspControlImplementationDetailRoute,
'/ssp/<string:ssp_uid>/control-implementation/<string:control_identifier>',
)
api.add_resource(
SspObjectiveDetailRoute,
'/ssp/<string:ssp_uid>/control-implementation/<string:control_identifier>/objective/<string:statement_identifier>',
)
api.add_resource(
SspObjectiveDocumentRoute,
'/ssp/<string:ssp_uid>/control-implementation/<string:control_identifier>/objective/<string:statement_identifier>/document/<string:file_id>',
)新 route 路徑 api/oscal/routes/ssp/ssp_control_implementation_route.py 應符合 api/**/routes/*_route.py pattern,需確認不在 EXCLUDE_MODULES 中。
Files:
compliance-manager-be/app/project/service/oscal_project_service.py新增參數:
control_impl_domain_service: ControlImplementationDomainService,
objective_domain_service: ControlImplementationObjectiveDomainService,並存為 self._ctrl_impl_ds 和 self._obj_ds。
在 di_containers/project/project_containers.py(或 oscal_project_service 的 DI 配置處)加入:
control_impl_domain_service=oscal_container.control_implementation_domain_service,
objective_domain_service=oscal_container.control_implementation_objective_domain_service,在 # 重新抓一次 AP (line ~244) 之後:
# Step 7.5:初始化 SSP Control Implementations + Objectives
if ap and ssp:
for group in (ap.groups or []):
for control in (group.controls or []):
ctrl_entity = ControlImplementationEntity(
system_security_plan_id=ssp.id,
control_identifier=control.control_id,
catalog_control_id=getattr(control, "catalog_control_id", None),
implementation_status="unknown", # 不可為 None,ORM 欄位 nullable=False
created_user=curr_user,
updated_user=curr_user,
)
ctrl_entity = self._ctrl_impl_ds.add(ctrl_entity)
# 重新取得帶 objectives 用的 ctrl map
all_ctrls = self._ctrl_impl_ds.get_all_by_ssp_id(ssp.id)
ctrl_map = {c.control_identifier: c for c in all_ctrls}
for task in (ap.tasks or []):
# 找到 task 對應的 control_identifier
# task 透過 assessment_task_controls 關聯到 ap_control
for task_ctrl in (getattr(task, "controls", []) or []):
ctrl_id_str = getattr(task_ctrl, "control_id", None)
if not ctrl_id_str:
continue
parent_ctrl = ctrl_map.get(ctrl_id_str)
if not parent_ctrl:
continue
obj_entity = ControlImplementationObjectiveEntity(
system_security_plan_id=ssp.id,
control_implementation_id=parent_ctrl.id,
control_identifier=ctrl_id_str,
statement_identifier=task.task_code or task.title or str(task.id),
implementation_status=None,
created_user=curr_user,
updated_user=curr_user,
)
self._obj_ds.add(obj_entity)注意: AP task 與 control 的關聯結構取決於
init_ap_controls_from_profile的實際回傳。上面的task.controls可能需要根據實際 AP task entity 的屬性調整。實作時須先確認ap.tasks[*]的資料結構,特別是如何從 task 反查所屬的control_id。可能需要改用assessment_task_controlsmapping 表來查詢。
cd ~/Projects/Jedicogy/module/jedi-python-package/jedi-oscal
pip install -e .psql -h $DB_HOST -U $DB_USER -d compliance_manager -f scripts/sql/ssp_control_implementation_migration.sqlpython main_app.py確認啟動無 ImportError。
# 取得 control implementations 列表
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000/api/1.0/ssp/{ssp_uid}/control-implementations
# 更新控制項描述
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"implementation_status":"implemented","implementation_description":"test"}' \
http://localhost:8000/api/1.0/ssp/{ssp_uid}/control-implementation/AC.L1-3.1.1
# 更新 AO 描述
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"implementation_description":"AO test"}' \
http://localhost:8000/api/1.0/ssp/{ssp_uid}/control-implementation/AC.L1-3.1.1/objective/[a]需求: /grc/project/<uid>/control-group/<uid>/controls/list 回傳每個控制項和 AO 的 SSP 實作現況。
Files:
infra/grc/repository/grc_control_repo_impl.py — LEFT JOIN SSP 表取得現況domain/grc/entities/grc_control_entity.py — 加 SSP 欄位infra/grc/mapper/grc_control_mapper.py — 加 SSP 映射app/grc/dto/control_dto.py — 加 SSP 欄位api/grc/serializers/control.py — 加 SSP response 欄位在 domain/grc/entities/grc_control_entity.py 的 __init__ 新增:
# SSP 現況
ssp_implementation_status: str = None,
ssp_implementation_description: str = None,
ssp_has_description: bool = False,並在 body 中:
self.ssp_implementation_status = ssp_implementation_status
self.ssp_implementation_description = ssp_implementation_description
self.ssp_has_description = ssp_has_description在同一檔案的 GrcAoSummaryEntity.__init__ 新增:
ssp_implementation_status: str = None,
ssp_implementation_description: str = None,
ssp_has_description: bool = False,在 infra/grc/repository/grc_control_repo_impl.py 中:
3a) 新增 import:
from jedi_oscal.infra.model.ssp.ssp_control_implementation import OscalSystemSecurityPlanControlImplementation
from jedi_oscal.infra.model.ssp.ssp_control_implementation_objective import OscalSspControlImplementationObjective3b) 在 Step 1(resolve AP ID)之後,查詢 SSP ID:
已有 assessment_plan_id,需透過 AP 的 ssp_id 取得 SSP:
from jedi_oscal.infra.model.ap.assessment_plan import OscalAssessmentPlan
ap_row = session.query(OscalAssessmentPlan.ssp_id).filter(
OscalAssessmentPlan.id == assessment_plan_id
).first()
ssp_id = ap_row.ssp_id if ap_row else None注意:
OscalAssessmentPlan.ssp_id是 soft ref(String),需透過 SSP uid → id 轉換, 或直接用system_security_plan_control_implementations.system_security_plan_idJOIN。 實作時確認 AP model 的ssp_id欄位型別。
3c) 在 main query 中加 LEFT JOIN SSP control implementation:
# SSP 現況 subquery
ssp_impl_status = session.query(
OscalSystemSecurityPlanControlImplementation.control_identifier,
OscalSystemSecurityPlanControlImplementation.implementation_status,
OscalSystemSecurityPlanControlImplementation.implementation_description,
).filter(
OscalSystemSecurityPlanControlImplementation.system_security_plan_id == ssp_id,
).subquery("ssp_impl")
# 在 main query 加 outerjoin
query = query.outerjoin(
ssp_impl_status,
OscalAssessmentPlanControl.control_id == ssp_impl_status.c.control_identifier,
).add_columns(
ssp_impl_status.c.implementation_status.label("ssp_implementation_status"),
ssp_impl_status.c.implementation_description.label("ssp_implementation_description"),
)3d) 在 entity mapping 時傳入 SSP 欄位:
ssp_implementation_status=getattr(row, "ssp_implementation_status", None),
ssp_implementation_description=getattr(row, "ssp_implementation_description", None),
ssp_has_description=bool(getattr(row, "ssp_implementation_description", None)),在 Step 9(batch fetch AO summaries)中,加一個批次查詢:
# 批次取得 SSP objectives
if ssp_id:
ssp_objectives = session.query(
OscalSspControlImplementationObjective.statement_identifier,
OscalSspControlImplementationObjective.control_identifier,
OscalSspControlImplementationObjective.implementation_status,
OscalSspControlImplementationObjective.implementation_description,
).filter(
OscalSspControlImplementationObjective.system_security_plan_id == ssp_id,
).all()
# 建立 lookup: (control_identifier, statement_identifier) → obj
ssp_obj_map = {}
for obj in ssp_objectives:
key = (obj.control_identifier, obj.statement_identifier)
ssp_obj_map[key] = obj在建立 GrcAoSummaryEntity 時,用 task.task_code 作為 statement_identifier 查詢:
ssp_obj = ssp_obj_map.get((control_identifier, task.task_code))
ao_entity.ssp_implementation_status = ssp_obj.implementation_status if ssp_obj else None
ao_entity.ssp_implementation_description = ssp_obj.implementation_description if ssp_obj else None
ao_entity.ssp_has_description = bool(ssp_obj.implementation_description) if ssp_obj else False在 infra/grc/mapper/grc_control_mapper.py 的 to_full_entity() 中加入 SSP 參數傳遞。
在 app/grc/dto/control_dto.py 的 GrcControlDto 新增:
ssp_implementation_status: Optional[str] = None
ssp_implementation_description: Optional[str] = None
ssp_has_description: bool = False在 from_entity() 中加入映射。
ssp_implementation_status: Optional[str] = None
ssp_implementation_description: Optional[str] = None
ssp_has_description: bool = False在 api/grc/serializers/control.py 的 ControlResponseSchema 新增:
ssp_implementation_status = fields.Str(allow_none=True)
ssp_implementation_description = fields.Str(allow_none=True)
ssp_has_description = fields.Bool(dump_default=False)ssp_implementation_status = fields.Str(allow_none=True)
ssp_implementation_description = fields.Str(allow_none=True)
ssp_has_description = fields.Bool(dump_default=False)| 決策 | 理由 |
|---|---|
| Objective 新表放 jedi-oscal 而非主專案 | SSP 是 OSCAL 核心概念,應與 catalog/profile/ap 同套件 |
| control_identifier 用 VARCHAR(100) 而非 FK | 規格要求 soft reference,避免跨 Round AP 更新影響 SSP |
| implementation_status 允許 NULL | 表示「尚未填寫」,區別於任何有效狀態 |
| reference_documents 用 JSONB 而非關聯表 | 文件數量少(通常 1-3 筆),JSONB 更簡潔 |
| 文件上傳 API(2.4)暫不在此 plan 完整實作 | 需整合 jedi-file-upload,可作為獨立 Task 後續擴充 |
| UniqueConstraint 改用 control_identifier | 原 catalog_control_id 改 nullable 後不適合做 unique key |