前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >DevSecOps 管道: 使用Jenkins自动化CI/CD管道以实现安全的多语言应用程序

DevSecOps 管道: 使用Jenkins自动化CI/CD管道以实现安全的多语言应用程序

作者头像
DevOps云学堂
发布2023-11-29 18:23:16
2880
发布2023-11-29 18:23:16
举报
文章被收录于专栏:DevOps持续集成DevOps持续集成

本篇文章是「DevOps云学堂」与你共同进步的第 61


DevSecOps 流程 先决条件: 1) Git 2) Jenkins 3) Sonar-Scanner 4) Snyk 5) Java、Maven、Node.js、Python 等(您为项目选择的语言将取决于适用的安装要求。 6) Docker 7) Aqua Trivy 8) Kubernetes 9) Zaproxy

Jenkinsfile(Groovy 脚本)

代码语言:javascript
复制
// Define the detectJavaVersion function outside of the pipeline block
def detectJavaVersion() {
    def javaVersionOutput = sh(script: 'java -version 2>&1', returnStatus: false, returnStdout: true).trim()
    def javaVersionMatch = javaVersionOutput =~ /openjdk version "(\d+\.\d+)/

    if (javaVersionMatch) {
        def javaVersion = javaVersionMatch[0][1]

        if (javaVersion.startsWith("1.8")) {
            return '8'
        } else if (javaVersion.startsWith("11")) {
            return '11'
        } else if (javaVersion.startsWith("17")) {
            return '17'
        } else {
            error("Unsupported Java version detected: ${javaVersion}")
        }
    } else {
        error("Java version information not found in output.")
    }
}
pipeline {
    agent any
    environment {
        SONARCLOUD = 'Sonarcloud'
        SNYK_INSTALLATION = 'snyk@latest'
        SNYK_TOKEN = 'Snyk'
        DOCKER_REGISTRY_CREDENTIALS = 'Docker_Server'
        DOCKER_IMAGE = 'ganesharavind124/anacart:latest'
        DOCKER_TOOL = 'Docker'
        DOCKER_URL = 'https://index.docker.io/v1/'
        KUBE_CONFIG = 'kubernetes'
    }
    stages {
        stage('Clean Workspace') {
            steps {
                cleanWs()
            }
        }
        stage('Git-Checkout') {
            steps {
                checkout scm
            }
        }
        // /opt/sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner
        stage('Compile and Run Sonar Analysis') {
            steps {
                script {
                    withSonarQubeEnv(credentialsId: SONARCLOUD, installationName: 'Sonarcloud') {
                        try {
                            if (fileExists('pom.xml')) {
                                sh 'mvn verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar'
                            } else if (fileExists('package.json')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_testjs -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('go.mod')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_go -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('Gemfile')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_ruby -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else if (fileExists('requirements.txt')) {
                                sh "${sonarscanner} -Dsonar.organization=jenkeen -Dsonar.projectKey=jenkeen_python -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=b8c55c159b1fd559baaccf9bee42344faed0a7b4"
                            } else {
                                currentBuild.result = 'FAILURE'
                                pipelineError = true
                                error("Unsupported application type: No compatible build steps available.")
                            }
                        } catch (Exception e) {
                            currentBuild.result = 'FAILURE'
                            pipelineError = true
                            error("Error during Sonar analysis: ${e.message}")
                        }
                    }
                }
            }
        }
        stage('snyk_analysis') {
            steps {
                script {
                    echo 'Testing...'
                    try {
                        snykSecurity(
                            snykInstallation: SNYK_INSTALLATION,
                            snykTokenId: SNYK_TOKEN,
                            failOnIssues: false,
                            monitorProjectOnBuild: true,
                            additionalArguments: '--all-projects --d'
                        )
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during snyk_analysis: ${e.message}")
                    }
                }
            }
        }
        stage('Detect and Set Java') {
            steps {
                script {
                    try {
                        def javaVersion = detectJavaVersion()
                        tool name: "Java_${javaVersion}", type: 'jdk'
                        sh 'java --version'
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during Java version detection: ${e.message}")
                    }
                }
            }
        }
        stage('Frontend Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('package.json')) {
                            //sh 'npm install --force'
                            //sh 'npm test'
                        } else {
                            echo 'No package.json found, skipping Frontend build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        error("Error during Frontend build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Java Spring Boot Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('pom.xml')) {
                            sh 'mvn clean package'
                            sh 'mvn test'
                        } else {
                            // If pom.xml doesn't exist, print a message and continue
                            echo 'No pom.xml found, skipping Java Spring Boot build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Java Spring Boot build and test: ${e.message}")
                    }
                }
            }
        }

        stage('.NET Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourSolution.sln')) {
                            sh 'dotnet build'
                            sh 'dotnet test'
                        } else {
                            // If YourSolution.sln doesn't exist, print a message and continue
                            echo 'No YourSolution.sln found, skipping .NET build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during .NET build and test: ${e.message}")
                    }
                }
            }
        }
        stage('PHP Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('composer.json')) {
                            sh 'composer install'
                            sh 'phpunit'
                        } else {
                            // If composer.json doesn't exist, print a message and continue 
                            echo 'No composer.json found, skipping PHP build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during PHP build and test: ${e.message}")
                    }
                }
            }
        }
        stage('iOS Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('YourProject.xcodeproj')) {
                            xcodebuild(buildDir: 'build', scheme: 'YourScheme')
                        } else {
                            // If YourProject.xcodeproj doesn't exist, print a message and continue
                            echo 'No YourProject.xcodeproj found, skipping iOS build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during iOS build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Android Build and Test') {
            steps {
                script {
                    try {
                        if (fileExists('build.gradle')) {
                            sh './gradlew build'
                            sh './gradlew test'
                        } else {
                            // If build.gradle doesn't exist, print a message and continue
                            echo 'No build.gradle found, skipping Android build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Android build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Ruby on Rails Build and Test') {
            steps {
                script {
                    try {
                        // Check if Gemfile.lock exists
                        if (fileExists('Gemfile.lock')) {
                            sh 'bundle install' // Install Ruby gem dependencies
                            sh 'bundle exec rake db:migrate' // Run database migrations
                            sh 'bundle exec rails test' // Run Rails tests (adjust as needed)
                        } else {
                            // If Gemfile.lock doesn't exist, print a message and continue
                            echo 'No Gemfile.lock found, skipping Ruby on Rails build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby on Rails build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Flask Build and Test') { // To build and run a Python Flask Framework Application
            steps {
                script {
                    try {
                        if (fileExists('app.py')) {
                            sh 'pip install -r requirements.txt' // Install dependencies
                            sh 'python -m unittest discover' // Run Flask unit tests
                        } else {
                            // If app.py doesn't exist, print a message and continue
                            echo 'No app.py found, skipping Flask build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Flask build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Django Build and Test') { // To build and run a Python Django Framework Application
            steps {
                script {
                    try {
                        if (fileExists('manage.py')) {
                            sh 'pip install -r requirements.txt' // Install dependencies
                            sh 'python manage.py migrate' // Run Django migrations
                            sh 'python manage.py test' // Run Django tests
                        } else {
                            // If manage.py doesn't exist, print a message and continue
                            echo 'No manage.py found, skipping Django build and test.'
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        error("Error during Django build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Rust Build and Test') { //To build and run a Rust Application
            steps {
                script {
                    try {
                        if (fileExists('Cargo.toml')) { // Check if Cargo.toml file exists 
                            env.RUST_BACKTRACE = 'full' // Set the RUST_BACKTRACE environment variable to full for better error messages
                            sh 'cargo build' // Build the Rust project
                            sh 'cargo test' // Run the Rust tests
                        } else {
                            // If Cargo.toml doesn't exist, print a message and continue
                            echo "No Cargo.toml file found. Skipping Rust build and test."
                        }
                    } catch (Exception e) {
                        // Set the build result to FAILURE and print an error message
                        currentBuild.result = 'FAILURE'
                        error("Error during Rust build and test: ${e.message}")
                    }
                }

            }
        }
        stage('Ruby Sinatra Build and Test') { //To build and run a Ruby Application
            steps {
                script {
                    try {
                        if (fileExists('app.rb')) { // Check if app.rb file exists                 
                            sh 'gem install bundler' // Install Bundler
                            sh 'bundle install' // Use bundle exec to ensure gem dependencies are isolated        
                            sh 'bundle exec rake test' // Run the Sinatra tests using Rake
                        } else {
                            // If app.rb doesn't exist, print a message and continue
                            echo "No app.rb file found. Skipping Ruby Sinatra build and test."
                        }
                    } catch (Exception e) {
                        // Set the build result to FAILURE and print an error message
                        currentBuild.result = 'FAILURE'
                        error("Error during Ruby Sinatra build and test: ${e.message}")
                    }
                }
            }
        }
        stage('Build and Push Docker Image') {
            steps {
                script {
                    try {
                        if (fileExists('Dockerfile')) {
                            withDockerRegistry(credentialsId: DOCKER_REGISTRY_CREDENTIALS, toolName: DOCKER_TOOL, url: DOCKER_URL) {
                                def dockerImage = docker.build(DOCKER_IMAGE, ".")
                                // Push the built Docker image
                                dockerImage.push()
                            }
                        } else {
                            echo "Dockerfile not found. Skipping Docker image build and push."
                        }
                    } catch (Exception e) {
                        currentBuild.result = 'FAILURE'
                        pipelineError = true
                        echo "Error during Docker image build and push: ${e.message}"
                    }
                }
            }
        }
        stage('Trivy Scan') {
            steps {
                script {
                    def trivyInstalled = sh(script: 'command -v trivy', returnStatus: true) == 0
                    def imageName = DOCKER_IMAGE
                if (trivyInstalled) {
                    sh "trivy image --format table ${imageName}"
                } else {
                    // Run trivy using Docker
                    sh "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --format table ${imageName}"
                }
                
            }
            
        }
    }
    stage('Kubernetes Deployment') {
        steps {
            script {
                def configFile = 'deployment.yaml'
                def namespace = 'anacart' // Replace 'your-namespace' with your actual namespace 
    
                if (fileExists(configFile)) {
                     kubernetesDeploy(configs: configFile, kubeconfigId: KUBE_CONFIG, namespace: namespace)
                } else {
                    error("Error: $configFile does not exist")
                    currentBuild.result = 'FAILURE'
                    pipelineError = true
                }
            }
        }
    }
    stage('Run DAST Using ZAP') {
      steps {
        script {
          try {
            def targetURL =  "http://192.168.58.2:32765" // Use the obtained service URL as the target URL
            def zapCommand = "zaproxy -cmd -quickurl ${targetURL}"
            //sh(zapCommand)
            sh("echo zap_report.html")
            //archiveArtifacts artifacts: 'zap_report.html'
          } catch (Exception e) {
            currentBuild.result = 'FAILURE'
            error("Error during ZAP DAST: ${e.message}")
          }
        }
      }
    }
  }
}

简介

在当今快节奏的软件开发环境中,实施高效的 CI/CD 管道至关重要。本博客概述了使用 Jenkins 构建强大的 CI/CD 管道、集成各种工具以实现多语言应用程序的无缝自动化、安全性和部署的旅程。 准备阶段: 这个项目涉及编排一个 CI/CD 管道,其中包括 GitSonarCloudSynk多语言构建自动化DockerAqua TrivyKubernetesZAP Proxy。利用 Jenkins 的灵活性和 Groovy 脚本编写功能,我简化了这些将工具整合到一个有凝聚力的管道中。

管道配置

进入管道作业的配置页面。将打开此页面。在那里添加您的 Jenkins管道脚本路径。有两种选择。 1. 管道脚本:在这里,您可以轻松编写自己的脚本。 2. 来自 SCM 的管道:它将使用 SCM 存储库的 Jenkins 文件。 这里我选择第二个选项:

因此,选择您的 SCM 并提供您的分支和存储库的 URL,并在脚本路径中提及您的 Jenkinsfile。

第 1 阶段(清理工作区)

在此阶段,我们将清理工作区,其中之前部署的文件和文档,在此阶段完成后,git 将拉取新更新的文件并运行新的所有内容。

第 2 阶段(Git Checkout)

我们在项目中使用了多种源代码管理系统,包括GitHub、GitLab、AWS codecommit,以及bitbucket、SVN、TFS等;但是,我没有将该信息包含在流程图中。

git 配置: 在上面的 SCM 中提供您的 Git 详细信息;因此,请使用 SCM 中的 git 详细信息的 URL 和分支名称来更新它们。

git 签出: 注意:如果您的 git 存储库是私有的,您应该向您的 Jenkins 帐户提供您的 Gitlab 个人访问令牌或 git 凭据。

第 3 阶段(SonarCloud)

SonarCloud 用于执行 SAST 代码质量扫描,因此通过添加个人访问令牌或身份验证令牌将其与 Jenkins 集成。您还可以将声纳扫描仪工具称为声纳扫描仪,或您选择的任何其他工具,并且不要忘记将其包含在您的管道中。

有两种选项可以运行 sonarcloud : 1) 在 git 存储库中创建 sonar-project-properties 文件,并提供 sonarcloud 详细信息,如下所示:

sonar-project.properties 在这里,将您的声纳扫描仪路径以及您的 pom.xml、csproj、解决方案文件、包添加到 Jenkins 管道脚本中。Json、Gem 文件、requirement.txt 等 2)您可以直接在Jenkins文件中提及您的sonarcloud脚本。

编译并运行Sonar分析

第 4 阶段(Synk安全漏洞扫描)

Synk 用于执行安全漏洞扫描,因此通过为其提供个人访问令牌或身份验证令牌将其与 Jenkins 集成。您还可以将您的 synk 安装工具称为 Snyk@latest,或者您选择的任何其他工具,并且不要忘记将其包含在您的管道中。

现在,在您的管道中提及您的安装和 Snyk 令牌的名称,以便它知道您正在尝试访问哪个 API。

第 5 阶段(Java 检测)

正如我之前指出的,Java 可能会被自动检测到,您将能够看到它是否受支持。因此,在执行此操作之前,请确保您已在 Jenkins 工具中设置了 JDK。

检测Java版本,所以这里 java 检测并设置 java pipeline 脚本如下所示:

检测并设置 Java

第 6 阶段(多语言构建和部署)

在这个阶段,我提供了多种编程语言,包括前端、后端、iOS、Android、Ruby、Flask等等。根据我提供的语言,系统将从您的存储库中识别源代码,并根据我们之前讨论的管道脚本安装、构建和执行测试。

Java、Maven、Node.js、Python 等(您为项目选择的语言将取决于适用的安装要求。)在这里,我在项目中使用 Node.js。

多语言构建阶段,您可以在上图中看到多语言构建的管道脚本。

第 7 阶段(Docker 构建和推送)

在此阶段,我们将在构建源代码后对我们的项目进行 dockerize。我们的pipeline脚本会自动识别dockerfile是否存在,如果不存在则生成dockerfile,否则会显示dockerfile not find。

注意:请确保在环境阶段正确指定 Docker 镜像的名称(变量名称将自动识别并获取镜像名称)。Dockerfile 名称区分大小写,在 Jenkins 中添加 docker 工具和 docker API。

构建并推送 Docker 镜像 在此阶段,我们将把我们的镜像推送并存储在 Docker Hub、AWS ECR、GCP GCR、Harbor 等容器注册表中。在本例中,我通过提供我的凭据并指示我要推送到我的集线器存储库的 Docker API 来使用 Docker Hub。在此之前,不要忘记在 Docker Hub 上设置一个存储库。

要链接到您的容器注册表,请确保向 Jenkins 提供您的凭据或个人访问令牌。在环境阶段提及您的凭据。

环境 注意:通过在本地使用 docker run 命令,您可以验证 Docker 映像是否已启动并正在运行。

第 8 阶段(Aqua Trivy 镜像扫描)

现在 Docker 构建已经完成并且我们的映像已成功生成,是时候通过扫描来检测任何漏洞了。我们将使用 Aqua Trivy Scan 进行图像扫描。

验证 Aqua Trivy 是否已安装在您的本地系统上。如果您的系统上尚未安装 trivy,请从 docker 获取它并运行 trivy 映像。完成后,尝试使用 docker trivy image 扫描您的映像。使用以下 docker trivy 命令将映像名称放在映像命令后面: docker run ghcr.io/aquasecurity/trivy:最新镜像 DOCKER_IMAGE

Aqua Trivy 扫描 在这里提及您的 docker 映像,它将扫描并检测漏洞。

第 9 阶段(Kubernetes)

这是我们现在所处的主要阶段。到目前为止,一切都按计划进行,我们构建、部署和 Docker 化了我们的镜像并将其推送到中心。但是,我们必须在运行时托管我们的程序。流程是怎样的?应用 Kubernetes 是前进的方向。

在集成 Kubernetes 和 Jenkins 之前,请确保您已安装集群;它们是 minikube、kind 还是 kubeadm 并不重要。如果您使用负载均衡器,请安装 kubeadm 并构建您的主节点和工作节点。如果您使用的是 nodeport,请在 Jenkins 从机上安装 minikube 或 kind 集群。

注意:您可以使用 kube 配置文件将 Jenkins 与 Kubernetes 集群集成。

Kubernetes 部署 在环境阶段,提供您的 kube 配置凭据并添加部署.yaml 文件的名称来代替配置文件。

环境 在成功创建部署后,应用程序现在将在您的 Pod 上运行。您可以通过使用服务名称运行 (kubectl get svc) 进行测试。如果您使用负载均衡器,您将收到外部 IP 并能够通过它访问您的应用程序。 如果您使用 minikube 运行(minikube 服务 MY-SERVICE-NAME),您将收到您的 IP 和端口号,并能够通过它访问您的应用程序。

第 10 阶段(Zaproxy 测试)

我们已经进行了 SAST 扫描和应用测试;展望未来,我们将执行 DAST,其目的是在整个软件开发和测试阶段协助检测 Web 应用程序中的安全漏洞。

基本上,ZAP 测试将涉及使用该 URL 来测试 PROD 或 DEV 中托管的应用程序。我们将使用各种扫描方法,包括蜘蛛、主动、被动、模糊器、代理拦截和脚本攻击。不过,目前我只是进行基本的 zap 测试,生成并向我们提供报告。

确保 ZAPROXY 已安装在您的本地或实例或服务器系统上。 这里我使用了 minikube,所以我直接在 Jenkins 管道中提供了 URL。

使用 Zaproxy 进行 DAST 扫描 使用Loadbalancer时,会自动执行zap命令,无需手动输入,并且自动生成IP和端口。使用以下脚本自动检测 URL。

让我们通过运行管道脚本来实际看看:

创建管道作业并为其指定一个您选择的名称,例如 Devsecops。

创建新的管道作业: 创建管道作业后将如下所示

新的 DevSecOps 工作 进入管道作业的配置页面。将打开此页面。在那里添加您的 Jenkins 管道脚本。 有两种选择。 1)管道脚本:在这里,您可以轻松编写自己的脚本。 2)来自 SCM 的管道:它将使用 SCM 存储库的 Jenkins 文件。

管道配置 我从 SCM 选择 Pipeline 脚本,因为我的 SCM 中有 Jenkinsfile(groovy 脚本)。 我也会向您展示另一种方法第二种方法。

在保存和应用之前检查所有行、大括号和凭据。您还应该确保环境和阶段中的变量名称相同,因为很多人在这个特定区域会犯错误。接下来,单击“应用”。如果遇到任何问题,该行中会出现一个 X。如果您更改“保存”,页面将重定向到主站点。之后,单击“立即构建”按钮。

构建历史 作业将开始执行。您可以在控制台查看作业结果,看看是否有问题。

控制台输出 我们可以看到我们的工作输出已经成功。来输出一下吧

管道构建阶段 Snyk:

Snyk

SonarCloud:

Docker hub:

Aqua Trivy 报告:

Kubernetes 部署:

ZapProxy:

ANACART(我们可以看到我们的应用程序已成功托管):

文章翻译 https://medium.com/@ganesharavind124/devsecops-pipeline-automating-ci-cd-pipeline-for-secure-multi-language-applications-using-jenkins-e66107dc4c04

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DevOps云学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 管道配置
  • 第 1 阶段(清理工作区)
  • 第 2 阶段(Git Checkout)
  • 第 3 阶段(SonarCloud)
  • 第 4 阶段(Synk安全漏洞扫描)
  • 第 5 阶段(Java 检测)
  • 第 6 阶段(多语言构建和部署)
  • 第 7 阶段(Docker 构建和推送)
  • 第 8 阶段(Aqua Trivy 镜像扫描)
  • 第 9 阶段(Kubernetes)
  • 第 10 阶段(Zaproxy 测试)
  • 让我们通过运行管道脚本来实际看看:
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档