Своевременное выявление уязвимостей в коде является одной из важнейших задач конвейера CI/CD, ведь чем раньше мы обнаружим ошибку в коде, тем дешевле нам обойдется ее исправление. Для решения этой задачи существует множество различных решений. Но если мы используем конвейер CI/CD то нам необходимо интегрировать наш анализатор в этот процесс. Однако, в GitLab имеется своя функциональность для анализа исходного кода. В этой статье мы настроим GitLab SAST для автоматического анализа исходного кода на наличие уязвимостей.
Готовим конфигурацию
Для начала нам необходимо включить GitLab SAST в нашем репозитории. Для этого нужно создать специальный файл конфигурации SAST в папке: ci/SAST.gitlab-ci.yml.
variables:
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_ANALYZERS: ""
DEFAULT_SAST_EXCLUDED_PATHS: ""
SAST_EXCLUDED_PATHS: "$DEFAULT_SAST_EXCLUDED_PATHS"
SCAN_KUBERNETES_MANIFESTS: "false"
sast:
extends: .sast
stage: sast
artifacts:
paths:
- gl-sast-report.json
expire_in: 1 day
access: 'developer'
reports:
sast: gl-sast-report.json
rules:
- when: never
variables:
SEARCH_MAX_DEPTH: 4
script:
- echo "$CI_JOB_NAME is used for configuration only"
Мы будем постепенно заполнять этот файл содержимым для того, чтобы было понятно какой раздел файла за что отвечает. Сейчас мы объявили ряд переменных и разместили раздел SAST. Далее нам необходимо определить анализаторы SAST, которые мы будем использовать
Начнем с GitLab Advanced SAST. Этот анализатор можно использовать для Python, Go, Java, JavaScript, TypeScript. Но здесь есть важный нюанс - GitLab Advanced SAST доступен исключительно для пользователей уровня Ultimate. Если вы используете уровень Free или Premium, рассмотрите возможность использования анализаторов SAST по умолчанию.
В файл SAST.gitlab-ci.yml добавляем следующее:
gitlab-advanced-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gitlab-advanced-sast:1$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /gitlab-advanced-sast/
when: never
- if: $GITLAB_ADVANCED_SAST_ENABLED != 'true'
when: never
- exists:
- '**/*.py'
- '**/*.go'
- '**/*.java'
- '**/*.js'
- '**/*.ts'
Для языков PHP, Swift, Scala, Ruby можно воспользоваться анализатором Semgrep SAST и тогда в файл нужно добавить:
semgrep-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:5$SAST_IMAGE_SUFFIX"
rules:
- exists:
- '**/*.php'
- '**/*.swift'
- '**/*.scala'
- '**/*.rb'
Боремся за читаемость отчетов
По умолчанию GitLab SAST выдает отчеты в формате JSON, но давайте преобразуем их в HTML и JUnit XML для лучшей наглядности.
Для этого нам необходимо добавить в файл следующее:
sast_to_html:
extends: .sast-analyzer
image: python:3.9
script:
- cp gl-sast-report.json ci/
- cd ci
- python3 convert_sast_to_html.py gl-sast-report.json index.html
- python3 junit_convertor.py gl-sast-report.json report.xml
artifacts:
paths:
- ci/index.html
- ci/report.xml
- gl-sast-report.json
reports:
junit: ci/report.xml
expire_in: 1 day
В представленном YAML можно заметить упоминание скриптов на Python. Чтобы обеспечить визуализацию уязвимостей в виджетах GitLab Merge Request, мы сначала используем скрипт для преобразования отчета JSON SAST в формат JUnit:
import json
import sys
import xml.etree.ElementTree as ET
def convert_json_to_junit(json_file, output_file):
try:
with open(json_file, "r") as f:
json_data = json.load(f)
testsuites = ET.Element("testsuites")
testsuite = ET.SubElement(testsuites, "testsuite", name="Security Scan", tests=str(len(json_data["vulnerabilities"])), failures=str(len(json_data["vulnerabilities"])))
for vulnerability in json_data["vulnerabilities"]:
test_case = ET.SubElement(testsuite, "testcase", name=vulnerability["name"], classname=vulnerability["category"])
failure_message = f"{vulnerability['description']}\nSeverity: {vulnerability['severity']}\n"
failure_message += f"File: {vulnerability['location']['file']} (Line {vulnerability['location']['start_line']})\n"
if "mitigation" in vulnerability:
failure_message += f"Mitigation: {vulnerability['mitigation']}\n"
failure_element = ET.SubElement(test_case, "failure", message=f"{vulnerability['name']} detected")
failure_element.text = failure_message
tree = ET.ElementTree(testsuites)
ET.indent(tree, space=" ")
tree.write(output_file, encoding="utf-8", xml_declaration=True)
print(f"JUnit report generated: {output_file}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python convert_json_to_junit.py <input_json_file> <output_xml_file>")
sys.exit(1)
json_file = sys.argv[1]
output_file = sys.argv[2]
convert_json_to_junit(json_file, output_file)
А затем мы хотим представить наш итоговый отчет о найденных уязвимостях в формате HTML. Для этого нам потребуется второй скрипт.
import json
import sys
def parse_json_to_html(json_data):
vulnerabilities = json_data.get('vulnerabilities', [])
html = '''
<html>
<head>
<title>SAST Report</title>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f3f4f6;
margin: 0;
padding: 30px;
}
h2 {
color: #2c3e50;
text-align: center;
font-size: 28px;
font-weight: 600;
margin-bottom: 20px;
}
table {
border-collapse: collapse;
width: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
background: linear-gradient(135deg, #f0f8ff, #fdfdfd);
border-radius: 12px;
overflow: hidden;
}
th, td {
padding: 15px;
text-align: left;
font-size: 14px;
}
th {
background-color: #34495e;
color: white;
text-transform: uppercase;
font-size: 16px;
}
td {
background-color: #ecf0f1;
color: #2c3e50;
border-bottom: 1px solid #ddd;
}
tr:nth-child(odd) td {
background-color: #f9fafb;
}
tr:nth-child(even) td {
background-color: #ffffff;
}
tr:hover td {
background-color: #dcdfe1;
cursor: pointer;
transition: background-color 0.3s ease;
}
.severity-high {
background-color: #e74c3c; /* Red for High */
padding: 5px 10px;
border-radius: 5px;
text-align: center;
font-weight: bold;
color: white;
}
.severity-medium {
background-color: #f39c12; /* Orange for Medium */
padding: 5px 10px;
border-radius: 5px;
text-align: center;
font-weight: bold;
color: white;
}
.severity-low {
background-color: #f1c40f; /* Yellow for Low */
padding: 5px 10px;
border-radius: 5px;
text-align: center;
font-weight: bold;
color: white;
}
</style>
</head>
<body>
<h2>GitLab SAST Report</h2>
<table>
<tr>
<th>Name</th>
<th>Description</th>
<th>Severity</th>
<th>File</th>
<th>Line</th>
</tr>
'''
for item in vulnerabilities:
severity = item.get('severity', '').strip().lower()
severity_class = ''
if severity == 'high':
severity_class = 'severity-high'
elif severity == 'medium':
severity_class = 'severity-medium'
elif severity == 'low':
severity_class = 'severity-low'
else:
severity_class = 'severity-low'
html += f'''
<tr>
<td>{item.get('name', '')}</td>
<td>{item.get('description', '')}</td>
<td class="{severity_class}">{item.get('severity', '')}</td>
<td>{item.get('location', {}).get('file', '')}</td>
<td>{item.get('location', {}).get('start_line', '')}</td>
</tr>
'''
html += '''
</table>
</body>
</html>
'''
return html
def main():
if len(sys.argv) != 3:
print("Usage: python convert_sast_to_html.py <input_json_file> <output_html_file>")
sys.exit(1)
input_json_file = sys.argv[1]
output_html_file = sys.argv[2]
with open(input_json_file, 'r') as file:
json_data = json.load(file)
html_content = parse_json_to_html(json_data)
with open(output_html_file, 'w') as file:
file.write(html_content)
if __name__ == "__main__":
main()
В итоге, отчет JUnit XML используется для отображения результатов SAST в виджете Merge Request.

А использование формата HTML позволяет использовать GitLab Pages для лучшей визуализации результатов.

Теперь нам необходимо интегрировать наш SAST в основной конвейер.
Интегрируем в конвейер
Итак, когда мы настроили GitLab SAST и отчетность, пришло время интегрировать его в наш конвейер CI/CD. Таким образом, мы гарантируем, что каждый коммит кода и запрос на слияние будут автоматически сканироваться на наличие уязвимостей безопасности. Чтобы включить автоматическое сканирование SAST, добавьте следующую задачу SAST в ваш основной файл конфигурации .gitlab-ci.yml:
stages:
- sast # Define a dedicated SAST stage in the pipeline
include:
- template: ci/SAST.gitlab-ci.yml # Includes GitLab’s predefined SAST template
stages:
- build # Typically used for compiling the application, running unit tests, etc.
- sast # Dedicated to Static Application Security Testing (SAST) to identify security vulnerabilities in the codebase before deployment.
.sast:
variables:
SAST_DISABLE_DIND: "true" # Disables Docker-in-Docker (DinD) when running SAST scans
SEARCH_MAX_DEPTH: 10 # Defines how deep the SAST scanner should search for source code files
Как видно, мы добавили наш файл с настройками SAST в основной файл описания конвейера GitLab.
Запускаем SAST в GitLab
После настройки GitLab SAST запускается автоматически на основе конфигурации конвейера и определенных правил. Так, появление любого нового коммита, отправленного в ветку репозитория, запускает конвейер, и, если правила позволяют, SAST сканирует код на наличие уязвимостей.
Также, при создании запроса на слияние GitLab CI/CD запускает сканирование SAST для анализа изменений перед слиянием, что помогает выявить проблемы безопасности на ранних этапах процесса разработки.
И, наконец, если у нас запускается запланированный конвейер, SAST выполняется периодически (например, ежедневно или еженедельно) для обеспечения непрерывного мониторинга безопасности, даже когда активная разработка не ведется.
Выполнение на основе правил
Фактическое выполнение SAST зависит от правил, определенных в .gitlab-ci.yml. Эти правила определяют, при каких условиях запускается задание SAST. Приведем несколько примеров:
Запуск только на определенных ветках (only: [main])
При открытии или обновлении запроса на слияние (if: „$CI_PIPELINE_SOURCE == «merge_request_event»“)
По расписанию (if: „$CI_PIPELINE_SOURCE == «schedule»“)
При внесении изменений в определенные файлы (changes: [«**/*.py», «**/*.js»])
Правильно настроив правила, команды могут контролировать, когда и как часто запускается SAST, оптимизируя эффективность конвейера и сохраняя при этом лучшие практики безопасности.
В завершении приведем полный пример файла ci/SAST.gitlab-ci.yml, который мы использовали для анализа исходного кода.
variables:
SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_ANALYZERS: ""
DEFAULT_SAST_EXCLUDED_PATHS: ""
SAST_EXCLUDED_PATHS: "$DEFAULT_SAST_EXCLUDED_PATHS"
SCAN_KUBERNETES_MANIFESTS: "false"
sast:
extends: .sast
stage: sast
artifacts:
paths:
- gl-sast-report.json
expire_in: 1 day
access: 'developer'
reports:
sast: gl-sast-report.json
rules:
- when: never
variables:
SEARCH_MAX_DEPTH: 4
script:
- echo "$CI_JOB_NAME is used for configuration only, and its script should not be executed"
- exit 1
.sast-analyzer:
extends: sast
allow_failure: true
script:
- /analyzer run
gitlab-advanced-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: '1'
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/gitlab-advanced-sast:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /gitlab-advanced-sast/
when: never
- if: $GITLAB_ADVANCED_SAST_ENABLED != 'true' && $GITLAB_ADVANCED_SAST_ENABLED != '1'
when: never
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bsast_advanced\b/
exists:
- '**/*.py'
- '**/*.go'
- '**/*.java'
- '**/*.jsp'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.cjs'
- '**/*.mjs'
- '**/*.cs'
- '**/*.rb'
semgrep-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 5
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:
- if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /semgrep/
when: never
# In case gitlab-advanced-sast also runs, exclude files already scanned by gitlab-advanced-sast
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bsast_advanced\b/ &&
$SAST_EXCLUDED_ANALYZERS !~ /gitlab-advanced-sast/ &&
($GITLAB_ADVANCED_SAST_ENABLED == 'true' || $GITLAB_ADVANCED_SAST_ENABLED == '1')
variables:
SAST_EXCLUDED_PATHS: "$DEFAULT_SAST_EXCLUDED_PATHS, **/*.py, **/*.go, **/*.java, **/*.js, **/*.jsx, **/*.ts, **/*.tsx, **/*.cjs, **/*.mjs, **/*.cs, **/*.rb"
exists:
- '**/*.c'
- '**/*.cc'
- '**/*.cpp'
- '**/*.c++'
- '**/*.cp'
- '**/*.cxx'
- '**/*.h'
- '**/*.hpp'
- '**/*.scala'
- '**/*.sc'
- '**/*.php'
- '**/*.swift'
- '**/*.m'
- '**/*.kt'
## In case gitlab-advanced-sast already covers all the files that semgrep-sast would have scanned
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bsast_advanced\b/ &&
$SAST_EXCLUDED_ANALYZERS !~ /gitlab-advanced-sast/ &&
($GITLAB_ADVANCED_SAST_ENABLED == 'true' || $GITLAB_ADVANCED_SAST_ENABLED == '1')
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule"
exists:
- '**/*.py'
- '**/*.js'
- '**/*.jsx'
- '**/*.ts'
- '**/*.tsx'
- '**/*.cjs'
- '**/*.mjs'
- '**/*.c'
- '**/*.cc'
- '**/*.cpp'
- '**/*.c++'
- '**/*.cp'
- '**/*.cxx'
- '**/*.h'
- '**/*.hpp'
- '**/*.go'
- '**/*.java'
- '**/*.cs'
- '**/*.scala'
- '**/*.sc'
- '**/*.php'
- '**/*.swift'
- '**/*.m'
- '**/*.rb'
- '**/*.kt'
sast_to_html:
extends: .sast-analyzer
image: python:3.9
script:
- cp gl-sast-report.json ci/
- cd ci
- python3 convert_sast_to_html.py gl-sast-report.json index.html
- python3 junit_convertor.py gl-sast-report.json report.xml
artifacts:
paths:
- ci/index.html
- ci/report.xml
- gl-sast-report.json
reports:
junit: ci/report.xml
expire_in: 1 day
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule"
when: always
needs: # Ensure this job runs after the SAST jobs
- semgrep-sast
pages:
extends: .sast-analyzer
script:
- mkdir public
- mv ci/index.html public/
artifacts:
paths: ["public"]
expire_in: 1 day
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "schedule"
needs: # Ensure this job runs after the SAST jobs
- sast_to_html
Заключение
GitLab SAST, позволяет использовать проактивный подход к безопасности в жизненном цикле разработки. Благодаря автоматическому сканированию, подробным отчетам и бесшовной интеграции с GitLab CI/CD вы можете обнаруживать и устранять уязвимости безопасности на ранней стадии, прежде чем они приведут к дорогостоящим нарушениям.
Интегрируя SAST в ваш репозиторий GitLab, вы автоматизируете сканирование безопасности, снижаете риски и без особых усилий обеспечиваете соблюдение требований безопасности.
Также важно отметить: настройка и интеграция GitLab SAST — лишь один из элементов комплексного подхода к безопасности и качеству разработки. Помимо анализа исходного кода, DevOps‑командам необходимо выстраивать мониторинг, управлять микросервисами, автоматизировать инфраструктуру и эффективно работать с CI/CD.
Чтобы глубже разобраться в этих практиках, вы можете присоединиться к серии бесплатных открытых уроков курса «DevOps: практики и инструменты». Вас ждут четыре занятия:
28 августа в 20:00 — «Мониторинг и алертинг приложений с помощью Prometheus и Grafana»
9 сентября в 20:00 — «Service Mesh: как перестать беспокоиться и начать управлять микросервисами»
16 сентября в 20:00 — «Настройка GitLab Runners. Хаки по настройке и оптимизации сборок проектов»
24 сентября в 20:00 — «Автовебинар — Инфраструктура как код»
Кроме того, доступно бесплатное вступительное тестирование, которое позволит оценить ваши знания и навыки.
Также, если вам важно понять, как курс воспринимают другие участники, вы можете ознакомиться с отзывами. Это позволит увидеть, какие темы оказались наиболее полезными, как проходит обучение и что отмечают слушатели после прохождения программы.